Compare commits
2 Commits
SMA_Band_I
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 01365218af | |||
| 83fb215eec |
30
.dockerignore
Normal file
30
.dockerignore
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
!**/.gitignore
|
||||||
|
!.git/HEAD
|
||||||
|
!.git/config
|
||||||
|
!.git/packed-refs
|
||||||
|
!.git/refs/heads/**
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@
|
|||||||
/packages
|
/packages
|
||||||
/.cr
|
/.cr
|
||||||
/.idea
|
/.idea
|
||||||
|
/Bitget_Server/logs
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ VisualStudioVersion = 17.9.34902.65
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitgetApi", "BitgetApi\BitgetApi.csproj", "{09813A63-9EF4-4D6D-BA41-5F610909BDEC}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitgetApi", "BitgetApi\BitgetApi.csproj", "{09813A63-9EF4-4D6D-BA41-5F610909BDEC}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitget_Server", "Bitget_Server\Bitget_Server.csproj", "{725DC784-0243-4FDF-921E-91CF09580A03}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitget_Server", "Bitget_Server\Bitget_Server.csproj", "{22361711-30AD-4CFE-8D21-00868CA322EA}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitget_App", "Bitget_App\Bitget_App.csproj", "{992C7279-FE6D-430B-A0C8-EC9DDEC743CA}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -19,16 +17,10 @@ Global
|
|||||||
{09813A63-9EF4-4D6D-BA41-5F610909BDEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{09813A63-9EF4-4D6D-BA41-5F610909BDEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{09813A63-9EF4-4D6D-BA41-5F610909BDEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{09813A63-9EF4-4D6D-BA41-5F610909BDEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{09813A63-9EF4-4D6D-BA41-5F610909BDEC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{09813A63-9EF4-4D6D-BA41-5F610909BDEC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{725DC784-0243-4FDF-921E-91CF09580A03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{22361711-30AD-4CFE-8D21-00868CA322EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{725DC784-0243-4FDF-921E-91CF09580A03}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{22361711-30AD-4CFE-8D21-00868CA322EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{725DC784-0243-4FDF-921E-91CF09580A03}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{22361711-30AD-4CFE-8D21-00868CA322EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{725DC784-0243-4FDF-921E-91CF09580A03}.Release|Any CPU.Build.0 = Release|Any CPU
|
{22361711-30AD-4CFE-8D21-00868CA322EA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{992C7279-FE6D-430B-A0C8-EC9DDEC743CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{992C7279-FE6D-430B-A0C8-EC9DDEC743CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{992C7279-FE6D-430B-A0C8-EC9DDEC743CA}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
|
||||||
{992C7279-FE6D-430B-A0C8-EC9DDEC743CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{992C7279-FE6D-430B-A0C8-EC9DDEC743CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{992C7279-FE6D-430B-A0C8-EC9DDEC743CA}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -112,7 +112,6 @@
|
|||||||
<Compile Include="IndicatorConditionSetup\IchimokuConditionSetup.cs" />
|
<Compile Include="IndicatorConditionSetup\IchimokuConditionSetup.cs" />
|
||||||
<Compile Include="IndicatorConditionSetup\ImpulseMacdConditionSetup.cs" />
|
<Compile Include="IndicatorConditionSetup\ImpulseMacdConditionSetup.cs" />
|
||||||
<Compile Include="IndicatorConditionSetup\MovingAverageConditionSetup.cs" />
|
<Compile Include="IndicatorConditionSetup\MovingAverageConditionSetup.cs" />
|
||||||
<Compile Include="IndicatorConditionSetup\SMABandConditionSetup.cs" />
|
|
||||||
<Compile Include="IndicatorConditionSetup\TradingViewConditionSetup.cs" />
|
<Compile Include="IndicatorConditionSetup\TradingViewConditionSetup.cs" />
|
||||||
<Compile Include="IndicatorData\IchimokData.cs" />
|
<Compile Include="IndicatorData\IchimokData.cs" />
|
||||||
<Compile Include="IndicatorConditionSetup\IndicatorProcess.cs" />
|
<Compile Include="IndicatorConditionSetup\IndicatorProcess.cs" />
|
||||||
@@ -131,6 +130,7 @@
|
|||||||
<Compile Include="WebService\WebConnection.cs" />
|
<Compile Include="WebService\WebConnection.cs" />
|
||||||
<EmbeddedResource Include="MainForm.resx">
|
<EmbeddedResource Include="MainForm.resx">
|
||||||
<DependentUpon>MainForm.cs</DependentUpon>
|
<DependentUpon>MainForm.cs</DependentUpon>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="Properties\Resources.resx">
|
<EmbeddedResource Include="Properties\Resources.resx">
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
|||||||
@@ -48,15 +48,6 @@ namespace BitgetAPI
|
|||||||
MARGIN_보류 = 2
|
MARGIN_보류 = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMA Band Type
|
|
||||||
public enum SMABandType
|
|
||||||
{
|
|
||||||
LongBuy = 0,
|
|
||||||
LongSell = 1,
|
|
||||||
ShortBuy = 2,
|
|
||||||
ShortSell = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExchangePlace selectedChangePlace;
|
public ExchangePlace selectedChangePlace;
|
||||||
public PaymentType selectedPaymentType;
|
public PaymentType selectedPaymentType;
|
||||||
public BitgetProductType selectedBitgetProductType;
|
public BitgetProductType selectedBitgetProductType;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,304 +0,0 @@
|
|||||||
using BitgetAPI.Data;
|
|
||||||
using BitgetAPI.Data.IndicatorData;
|
|
||||||
using DevExpress.Utils.Extensions;
|
|
||||||
using DevExpress.XtraBars.Ribbon.Gallery;
|
|
||||||
using DevExpress.XtraBars.ViewInfo;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Media.Media3D;
|
|
||||||
using static BitgetAPI.CommonProperty;
|
|
||||||
using static DevExpress.Utils.Diagnostics.GUIResources;
|
|
||||||
using static System.Windows.Forms.VisualStyles.VisualStyleElement.ToolTip;
|
|
||||||
|
|
||||||
namespace BitgetApi.IndicatorConditionSetup
|
|
||||||
{
|
|
||||||
public class IchimokuResult
|
|
||||||
{
|
|
||||||
public DateTime Time { get; set; }
|
|
||||||
public double TenkanSen { get; set; } // 전환선
|
|
||||||
public double KijunSen { get; set; } // 기준선
|
|
||||||
public double SenkouSpanA { get; set; } // 선행스팬1
|
|
||||||
public double SenkouSpanB { get; set; } // 선행스팬2
|
|
||||||
public double ChikouSpan { get; set; } // 후행스팬
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class SMABandConditionSetup
|
|
||||||
{
|
|
||||||
private readonly int rangeLookback = 12;
|
|
||||||
private double rangeThresholdLongPercent = 2;
|
|
||||||
private double rangeThresholdShortPercent = 2;
|
|
||||||
/// <summary>
|
|
||||||
/// false 는 구매 전, true는 판매 후
|
|
||||||
/// </summary>
|
|
||||||
private bool firstCloudFlag = true;
|
|
||||||
|
|
||||||
|
|
||||||
public bool Calculate(List<CandleData> candles, SMABandType type, bool flag = false)
|
|
||||||
{
|
|
||||||
|
|
||||||
var close = candles.Select(t => t.Close).ToList();
|
|
||||||
|
|
||||||
var high = candles.Select(t => t.High).ToList();
|
|
||||||
|
|
||||||
var low = candles.Select(t => t.Low).ToList();
|
|
||||||
|
|
||||||
var open = candles.Select(t => t.Open).ToList();
|
|
||||||
|
|
||||||
if (flag)
|
|
||||||
{
|
|
||||||
close.RemoveAt(close.Count - 1);
|
|
||||||
high.RemoveAt(high.Count - 1);
|
|
||||||
low.RemoveAt(low.Count - 1);
|
|
||||||
open.RemoveAt(open.Count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int i = close.Count - 1;
|
|
||||||
if (close.Count < 100 || high.Count < 100 || low.Count < 100 || open.Count < 100)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var avgLongList = new List<double>();
|
|
||||||
var avgShortList = new List<double>();
|
|
||||||
|
|
||||||
for (var cntData = high.Count - 120; cntData < high.Count; cntData++)
|
|
||||||
{
|
|
||||||
var lowestData = Lowest(low, cntData, 12, 1);
|
|
||||||
var highData = high[cntData];
|
|
||||||
|
|
||||||
avgLongList.Add((highData - lowestData)/lowestData * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var cntData = high.Count - 12; cntData < high.Count; cntData++)
|
|
||||||
{
|
|
||||||
var highestData = Highest(high, cntData, 4, 1);
|
|
||||||
var lowData = low[cntData];
|
|
||||||
|
|
||||||
avgShortList.Add((highestData - lowData) / highestData * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
rangeThresholdLongPercent = avgLongList.Average();
|
|
||||||
rangeThresholdShortPercent = avgShortList.Average() * 2.5;
|
|
||||||
|
|
||||||
double sma72 = CalculateSMA(close, i);
|
|
||||||
double prevSma72 = CalculateSMA(close, i - 1);
|
|
||||||
|
|
||||||
double sma24 = CalculateSMA(close, i, 24);
|
|
||||||
double prevSma24 = CalculateSMA(close, i - 1, 24);
|
|
||||||
|
|
||||||
|
|
||||||
// SMA4 관련
|
|
||||||
double sma4 = CalculateSMA(close, i, 4);
|
|
||||||
double prevSma4 = CalculateSMA(close, i - 1, 4);
|
|
||||||
double prev2Sma4 = CalculateSMA(close, i - 2, 4);
|
|
||||||
double prev3Sma4 = CalculateSMA(close, i - 3, 4);
|
|
||||||
|
|
||||||
// 밴드 계산
|
|
||||||
double sma4High = Highest(close, i, 12, 4);
|
|
||||||
double sma4Middle = Middle(close, i, 12, 4);
|
|
||||||
double sma4Low = Lowest(close, i, 12, 4);
|
|
||||||
|
|
||||||
double prevSma4High = Highest(close, i - 1, 12, 4);
|
|
||||||
double prevSma4Middle = Middle(close, i - 1, 12, 4);
|
|
||||||
double prevSma4Low = Lowest(close, i - 1, 12, 4);
|
|
||||||
|
|
||||||
double prev2Sma4High = Highest(close, i - 2, 12, 4);
|
|
||||||
double prev2Sma4Low = Lowest(close, i - 2, 12, 4);
|
|
||||||
double prev2Sma4Middle = Middle(close, i - 2, 12, 4);
|
|
||||||
|
|
||||||
double prev3Sma4High = Highest(close, i - 3, 12, 4);
|
|
||||||
double prev3Sma4Low = Lowest(close, i - 3, 12, 4);
|
|
||||||
double prev3Sma4Middle = Middle(close, i - 3, 12, 4);
|
|
||||||
|
|
||||||
|
|
||||||
var ichmok = GetIchimokuCloud(candles, i - 25);
|
|
||||||
var ichmokCurrent = GetIchimokuCloud(candles, i);
|
|
||||||
|
|
||||||
var cloudTopNow = Math.Max(ichmok.SenkouSpanA, ichmok.SenkouSpanB);
|
|
||||||
var cloudBottomNow = Math.Min(ichmok.SenkouSpanA, ichmok.SenkouSpanB);
|
|
||||||
|
|
||||||
var cloudTopAfter = Math.Max(ichmokCurrent.SenkouSpanA, ichmokCurrent.SenkouSpanB);
|
|
||||||
var cloudBottomAfter = Math.Min(ichmokCurrent.SenkouSpanA, ichmokCurrent.SenkouSpanB);
|
|
||||||
|
|
||||||
bool isSma4BelowCloud = low[i] < cloudBottomNow;
|
|
||||||
bool isSma4AboveCloud = high[i] > cloudTopNow;
|
|
||||||
|
|
||||||
bool greenCandle = open[i] < close[i];
|
|
||||||
bool redCandle = open[i] > close[i];
|
|
||||||
|
|
||||||
var isLongRecentTouch = false;
|
|
||||||
|
|
||||||
|
|
||||||
for (var cnt = 0; cnt < 10; cnt++)
|
|
||||||
{
|
|
||||||
if (Lowest(close, i - cnt, 12, 4) == CalculateSMA(close, i - cnt, 4) && Lowest(close, i - cnt - 1, 12, 4) <= CalculateSMA(close, i - cnt - 1, 4))
|
|
||||||
{
|
|
||||||
isLongRecentTouch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//high 데이터 비교(기본 12개)
|
|
||||||
var highestTempData = Highest(high, i, 12, 1);
|
|
||||||
|
|
||||||
// 진입/청산 조건
|
|
||||||
bool rawNewLong = highestTempData == high[i] && isLongRecentTouch && greenCandle && IsSideways(high, low, i, true, rangeThresholdLongPercent) && prevSma4 <= sma4 && sma4High == sma4 && prevSma4High >= prevSma4 && (prev2Sma4Middle - prevSma4Middle) > (prevSma4Middle - sma4Middle) && sma4Middle > sma72 && sma4 > cloudTopNow;
|
|
||||||
bool rawLongSell = (sma4Middle < sma72 && prevSma4Middle > prevSma72) || (sma4 == sma4Low);
|
|
||||||
|
|
||||||
bool rawNewShort = (sma72 < cloudTopNow && prevSma4Middle > prevSma24 && sma4Middle > cloudBottomNow && sma4 < sma72 && sma4High > sma72 && sma24 > sma4Middle && sma4 == sma4Low && IsSideways(high, low, i, false, rangeThresholdShortPercent) && prevSma4 > sma4 && sma4 < cloudBottomNow) || (sma4 < sma72 && sma4High > sma72 && sma24 < sma4Middle && sma4 == sma4Low && IsSideways(high, low, i, false, rangeThresholdShortPercent) && redCandle && sma4 < cloudBottomNow && sma4Middle < cloudTopNow);
|
|
||||||
bool rawShortSell = (sma4High <= sma4 && sma4High < sma72) || sma4 > sma72;
|
|
||||||
|
|
||||||
//bool rawNewLong = greenCandle && sma4Low < sma72 && IsSideways(high, low, i, true) && sma4 > cloudTopNow && prevSma4 <= prevSma4High && sma4 == sma4High && prevSma4 < sma4 && sma4Middle > sma72 && sma4 > cloudTopNow && ichmok.SenkouSpanA >= ichmok.SenkouSpanB && ichmokCurrent.SenkouSpanA >= ichmokCurrent.SenkouSpanB;
|
|
||||||
//sma4 == sma4High && prevSma4 < sma4High && sma4Middle > sma72 && sma4 > cloudTopNow && ichmokCurrent.SenkouSpanA > ichmokCurrent.SenkouSpanB && ichmok.SenkouSpanA >= ichmok.SenkouSpanB; // sma4 > cloudBottomNow && IsSideways(high, low, i, true) && isSma4AboveCloud && (sma4 == sma4High && prevSma4 <= prevSma4High && prev2Sma4 <= prev2Sma4High) && (sma4 >= prevSma4 && prevSma4 >= prev2Sma4 && prev2Sma4 >= prev3Sma4);
|
|
||||||
|
|
||||||
//bool rawLongSell = (sma4Middle < sma72 && prevSma4Middle > prevSma72) || (sma4 == sma4Low);
|
|
||||||
//(sma4Middle < sma72 && prevSma4Middle > prevSma72) || (sma4 < cloudBottomNow);// (sma4 <= prevSma4 && prevSma4 <= prev2Sma4 && sma4 < sma4Middle);
|
|
||||||
|
|
||||||
//cloudFlag &&
|
|
||||||
//bool rawNewShort = sma4 == sma4Low && prevSma4 > sma4Low && sma4Middle < sma72 && sma4 < cloudBottomNow;// sma4High < sma72 && prevSma4High > prevSma72;//sma4 < cloudTopNow && IsSideways(high, low, i, false) && isSma4BelowCloud && (sma4 == sma4Low && prevSma4 >= prevSma4Low && prev2Sma4 >= prev2Sma4Low) && (sma4 <= prevSma4 && prevSma4 <= prev2Sma4 && prev2Sma4 <= prev3Sma4);
|
|
||||||
|
|
||||||
//bool rawShortSell = (sma4Middle > sma72 && prevSma4Middle < prevSma72) || (sma4 > cloudTopNow);//(sma4 >= prevSma4 && prevSma4 >= prev2Sma4 && sma4 > sma4Middle);
|
|
||||||
|
|
||||||
// === 시그널 계산 ===
|
|
||||||
if (type == SMABandType.LongBuy)
|
|
||||||
{
|
|
||||||
return rawNewLong;
|
|
||||||
}
|
|
||||||
else if (type == SMABandType.ShortBuy)
|
|
||||||
{
|
|
||||||
return rawNewShort;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (type == SMABandType.LongSell)
|
|
||||||
{
|
|
||||||
return rawLongSell;
|
|
||||||
}
|
|
||||||
else if (type == SMABandType.ShortSell)
|
|
||||||
{
|
|
||||||
//if (rawShortSell)
|
|
||||||
// firstCloudFlag = true;
|
|
||||||
return rawShortSell;
|
|
||||||
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double CalculateSMA(List<double> data, int index, int period = 72)
|
|
||||||
{
|
|
||||||
var result = 0.0;
|
|
||||||
for (var i = 0; i < period; i++)
|
|
||||||
result += data[index - i];
|
|
||||||
|
|
||||||
|
|
||||||
return result / period;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<double> GetAvgList(List<double> source, int index, int period, int num)
|
|
||||||
{
|
|
||||||
var list = new List<double>();
|
|
||||||
|
|
||||||
for (var i = 0; i < period; i++)
|
|
||||||
{
|
|
||||||
list.Add(CalculateSMA(source, index - i, num));
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double Highest(List<double> source, int index, int period, int num)
|
|
||||||
{
|
|
||||||
|
|
||||||
return GetAvgList(source, index, period, num).Max();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private double Middle(List<double> source, int index, int period, int num)
|
|
||||||
{
|
|
||||||
|
|
||||||
return GetAvgList(source, index, period, num).Average();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private double Lowest(List<double> source, int index, int period, int num)
|
|
||||||
{
|
|
||||||
return GetAvgList(source, index, period, num).Min();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsSideways(List<double> high, List<double> low, int index, bool type, double rangeThresholdPercent)
|
|
||||||
{
|
|
||||||
double highestHigh = Highest(high, index, rangeLookback, 1);
|
|
||||||
double lowestLow = Lowest(low, index, rangeLookback, 1);
|
|
||||||
|
|
||||||
// long
|
|
||||||
if (type)
|
|
||||||
{
|
|
||||||
double range = high[index] - lowestLow;
|
|
||||||
return range > (highestHigh * rangeThresholdPercent / 100.0);
|
|
||||||
}
|
|
||||||
//short
|
|
||||||
else
|
|
||||||
{
|
|
||||||
double range = highestHigh - low[index];
|
|
||||||
return range > (lowestLow * rangeThresholdPercent / 100.0);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IchimokuResult GetIchimokuCloud(List<CandleData> candles, int i)
|
|
||||||
{
|
|
||||||
var highs = candles.Select(t => t.High).ToList();
|
|
||||||
var lows = candles.Select(t => t.Low).ToList();
|
|
||||||
var closes = candles.Select(t => t.Close).ToList();
|
|
||||||
|
|
||||||
var result = new IchimokuResult();
|
|
||||||
|
|
||||||
double tenkan = 0;
|
|
||||||
double kijun = 0;
|
|
||||||
double senkouA = 0;
|
|
||||||
double senkouB = 0;
|
|
||||||
double chikou = 0;
|
|
||||||
|
|
||||||
// Tenkan-sen (9)
|
|
||||||
if (i >= 8)
|
|
||||||
{
|
|
||||||
double max9 = highs.GetRange(i - 8, 9).Max();
|
|
||||||
double min9 = lows.GetRange(i - 8, 9).Min();
|
|
||||||
tenkan = (max9 + min9) / 2.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kijun-sen (26)
|
|
||||||
if (i >= 25)
|
|
||||||
{
|
|
||||||
double max26 = highs.GetRange(i - 25, 26).Max();
|
|
||||||
double min26 = lows.GetRange(i - 25, 26).Min();
|
|
||||||
kijun = (max26 + min26) / 2.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Senkou Span A (현재 시점에서 계산만, 시프트는 X)
|
|
||||||
if (tenkan != 0 && kijun != 0)
|
|
||||||
{
|
|
||||||
senkouA = (tenkan + kijun) / 2.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Senkou Span B (52)
|
|
||||||
if (i >= 51)
|
|
||||||
{
|
|
||||||
double max52 = highs.GetRange(i - 51, 52).Max();
|
|
||||||
double min52 = lows.GetRange(i - 51, 52).Min();
|
|
||||||
senkouB = (max52 + min52) / 2.0;
|
|
||||||
}
|
|
||||||
return new IchimokuResult
|
|
||||||
{
|
|
||||||
TenkanSen = tenkan,
|
|
||||||
KijunSen = kijun,
|
|
||||||
SenkouSpanA = senkouA,
|
|
||||||
SenkouSpanB = senkouB,
|
|
||||||
ChikouSpan = chikou
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -118,148 +118,128 @@ namespace BitgetAPI.WebService
|
|||||||
|
|
||||||
using (var wRes = (HttpWebResponse)wReq.GetResponse())
|
using (var wRes = (HttpWebResponse)wReq.GetResponse())
|
||||||
{
|
{
|
||||||
try
|
var respPostStream = wRes.GetResponseStream();
|
||||||
|
var readerPost = new StreamReader(respPostStream, Encoding.UTF8, true);
|
||||||
|
var resResult = readerPost.ReadToEnd();
|
||||||
|
switch (exchangeWebReqType)
|
||||||
{
|
{
|
||||||
var respPostStream = wRes.GetResponseStream();
|
#region Bitget
|
||||||
var readerPost = new StreamReader(respPostStream, Encoding.UTF8, true);
|
case CommonProperty.ExchangeWebReqType.BitgetMarket_Spot:
|
||||||
var resResult = readerPost.ReadToEnd();
|
{
|
||||||
switch (exchangeWebReqType)
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
{
|
var marketInfo = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
||||||
#region Bitget
|
var data = marketInfo.FindAll(x => x["coin"].ToString().Contains("USDT"));
|
||||||
case CommonProperty.ExchangeWebReqType.BitgetMarket_Spot:
|
}
|
||||||
|
break;
|
||||||
|
case CommonProperty.ExchangeWebReqType.BitgetMarket_Future:
|
||||||
|
{
|
||||||
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
|
var marketInfos = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
||||||
|
|
||||||
|
commonProperty.marketInfoList = new List<MarketInfo>();
|
||||||
|
|
||||||
|
foreach (var item in marketInfos)
|
||||||
{
|
{
|
||||||
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
MarketInfo marketInfo = new MarketInfo();
|
||||||
var marketInfo = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
marketInfo.MarketName = item["symbol"].ToString();
|
||||||
var data = marketInfo.FindAll(x => x["coin"].ToString().Contains("USDT"));
|
marketInfo.ExchangePlace = Enum.GetName(typeof(CommonProperty.ExchangePlace), commonProperty.selectedChangePlace);
|
||||||
|
marketInfo.lastPr = Convert.ToDouble(item["lastPr"]);
|
||||||
|
marketInfo.change24h = Convert.ToDouble(item["change24h"]);
|
||||||
|
marketInfo.baseVolume = Convert.ToDouble(item["baseVolume"]);
|
||||||
|
marketInfo.quoteVolume = Convert.ToDouble(item["quoteVolume"]);
|
||||||
|
marketInfo.high24h = Convert.ToDouble(item["high24h"]);
|
||||||
|
marketInfo.low24h = Convert.ToDouble(item["low24h"]);
|
||||||
|
marketInfo.open24h = Convert.ToDouble(item["open24h"]);
|
||||||
|
commonProperty.marketInfoList.Add(marketInfo);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case CommonProperty.ExchangeWebReqType.BitgetMarket_Future:
|
return commonProperty.marketInfoList;
|
||||||
|
}
|
||||||
|
case CommonProperty.ExchangeWebReqType.BitgetCandleStick_Coin:
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CommonProperty.ExchangeWebReqType.BitgetCandleStick_Future:
|
||||||
|
{
|
||||||
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
|
var datas = JsonConvert.DeserializeObject<List<List<object>>>(datalist["data"]?.ToString());
|
||||||
|
|
||||||
|
if (marketCandleDataList == null) return null;
|
||||||
|
|
||||||
|
var candleDataList = marketCandleDataList.candleDataList[interval];
|
||||||
|
|
||||||
|
foreach (var item in datas)
|
||||||
{
|
{
|
||||||
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
CandleData candleData = new CandleData();
|
||||||
var marketInfos = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
|
||||||
|
|
||||||
commonProperty.marketInfoList = new List<MarketInfo>();
|
candleData.Argument = Convertor.GetUnixtimeStampToDateTime(long.Parse(item[0].ToString()));
|
||||||
|
candleData.Open = Convert.ToDouble(item[1]);
|
||||||
|
candleData.High = Convert.ToDouble(item[2]);
|
||||||
|
candleData.Low = Convert.ToDouble(item[3]);
|
||||||
|
candleData.Close = Convert.ToDouble(item[4]);
|
||||||
|
candleData.Vol_C = Convert.ToDouble(item[5]);
|
||||||
|
candleData.Vol_USD = Convert.ToDouble(item[6]);
|
||||||
|
|
||||||
foreach (var item in marketInfos)
|
// 같은 시간이 존재하면 업데이트 : 1개밖에 없음
|
||||||
|
// 마지막 시간 데이터는 불완전 > 웹소켓 데이터와 합체
|
||||||
|
if (item == datas.Last())
|
||||||
{
|
{
|
||||||
MarketInfo marketInfo = new MarketInfo();
|
var timespan_add = new System.TimeSpan(0, 0, 0);
|
||||||
marketInfo.MarketName = item["symbol"].ToString();
|
|
||||||
marketInfo.ExchangePlace = Enum.GetName(typeof(CommonProperty.ExchangePlace), commonProperty.selectedChangePlace);
|
|
||||||
marketInfo.lastPr = Convert.ToDouble(item["lastPr"]);
|
|
||||||
marketInfo.change24h = Convert.ToDouble(item["change24h"]);
|
|
||||||
marketInfo.baseVolume = Convert.ToDouble(item["baseVolume"]);
|
|
||||||
marketInfo.quoteVolume = Convert.ToDouble(item["quoteVolume"]);
|
|
||||||
marketInfo.high24h = Convert.ToDouble(item["high24h"]);
|
|
||||||
marketInfo.low24h = Convert.ToDouble(item["low24h"]);
|
|
||||||
marketInfo.open24h = Convert.ToDouble(item["open24h"]);
|
|
||||||
commonProperty.marketInfoList.Add(marketInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return commonProperty.marketInfoList;
|
switch (interval)
|
||||||
}
|
|
||||||
case CommonProperty.ExchangeWebReqType.BitgetCandleStick_Coin:
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CommonProperty.ExchangeWebReqType.BitgetCandleStick_Future:
|
|
||||||
{
|
|
||||||
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
|
||||||
var datas = JsonConvert.DeserializeObject<List<List<object>>>(datalist["data"]?.ToString());
|
|
||||||
|
|
||||||
if (marketCandleDataList == null) return null;
|
|
||||||
|
|
||||||
var candleDataList = marketCandleDataList.candleDataList[interval];
|
|
||||||
|
|
||||||
foreach (var item in datas)
|
|
||||||
{
|
|
||||||
CandleData candleData = new CandleData();
|
|
||||||
|
|
||||||
candleData.Argument = Convertor.GetUnixtimeStampToDateTime(long.Parse(item[0].ToString()));
|
|
||||||
candleData.Open = Convert.ToDouble(item[1]);
|
|
||||||
candleData.High = Convert.ToDouble(item[2]);
|
|
||||||
candleData.Low = Convert.ToDouble(item[3]);
|
|
||||||
candleData.Close = Convert.ToDouble(item[4]);
|
|
||||||
candleData.Vol_C = Convert.ToDouble(item[5]);
|
|
||||||
candleData.Vol_USD = Convert.ToDouble(item[6]);
|
|
||||||
|
|
||||||
// 같은 시간이 존재하면 업데이트 : 1개밖에 없음
|
|
||||||
// 마지막 시간 데이터는 불완전 > 웹소켓 데이터와 합체
|
|
||||||
if (item == datas.Last())
|
|
||||||
{
|
{
|
||||||
var timespan_add = new System.TimeSpan(0, 0, 0);
|
case CommonProperty.MCandleTimeType.M1:
|
||||||
|
// 같은 분이면 무조건 업데이트
|
||||||
switch (interval)
|
timespan_add = new System.TimeSpan(0, 1, 0);
|
||||||
{
|
break;
|
||||||
case CommonProperty.MCandleTimeType.M1:
|
case CommonProperty.MCandleTimeType.M3:
|
||||||
// 같은 분이면 무조건 업데이트
|
// 같은 분이면 무조건 업데이트
|
||||||
timespan_add = new System.TimeSpan(0, 1, 0);
|
timespan_add = new System.TimeSpan(0, 3, 0);
|
||||||
break;
|
break;
|
||||||
case CommonProperty.MCandleTimeType.M3:
|
case CommonProperty.MCandleTimeType.M5:
|
||||||
// 같은 분이면 무조건 업데이트
|
timespan_add = new System.TimeSpan(0, 5, 0);
|
||||||
timespan_add = new System.TimeSpan(0, 3, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.M15:
|
||||||
case CommonProperty.MCandleTimeType.M5:
|
timespan_add = new System.TimeSpan(0, 15, 0);
|
||||||
timespan_add = new System.TimeSpan(0, 5, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.M30:
|
||||||
case CommonProperty.MCandleTimeType.M15:
|
timespan_add = new System.TimeSpan(0, 30, 0);
|
||||||
timespan_add = new System.TimeSpan(0, 15, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.H1:
|
||||||
case CommonProperty.MCandleTimeType.M30:
|
timespan_add = new System.TimeSpan(1, 0, 0);
|
||||||
timespan_add = new System.TimeSpan(0, 30, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.H2:
|
||||||
case CommonProperty.MCandleTimeType.H1:
|
timespan_add = new System.TimeSpan(2, 0, 0);
|
||||||
timespan_add = new System.TimeSpan(1, 0, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.H3:
|
||||||
case CommonProperty.MCandleTimeType.H2:
|
timespan_add = new System.TimeSpan(3, 0, 0);
|
||||||
timespan_add = new System.TimeSpan(2, 0, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.H4:
|
||||||
case CommonProperty.MCandleTimeType.H3:
|
timespan_add = new System.TimeSpan(4, 0, 0);
|
||||||
timespan_add = new System.TimeSpan(3, 0, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.H6:
|
||||||
case CommonProperty.MCandleTimeType.H4:
|
timespan_add = new System.TimeSpan(6, 0, 0);
|
||||||
timespan_add = new System.TimeSpan(4, 0, 0);
|
break;
|
||||||
break;
|
case CommonProperty.MCandleTimeType.H12:
|
||||||
case CommonProperty.MCandleTimeType.H6:
|
timespan_add = new System.TimeSpan(12, 0, 0);
|
||||||
timespan_add = new System.TimeSpan(6, 0, 0);
|
break;
|
||||||
break;
|
|
||||||
case CommonProperty.MCandleTimeType.H12:
|
|
||||||
timespan_add = new System.TimeSpan(12, 0, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sameCandleData_WebService = candleDataList.Find(i => candleData.Argument <= i.Argument &&
|
|
||||||
i.Argument < (candleData.Argument + timespan_add) && i.WebSocketUpdate == true);
|
|
||||||
|
|
||||||
if (sameCandleData_WebService != null)
|
|
||||||
{
|
|
||||||
// 종가 => 웹소켓 데이터 사용 (웹 서비스 호출 데이터 중 마지막 데이터는 캔들이 완성되지 않음)
|
|
||||||
// 시가 => 웹 서비스 데이터 사용
|
|
||||||
sameCandleData_WebService.Open = candleData.Open;
|
|
||||||
// 저가, 고가 => 비교해서 사용
|
|
||||||
sameCandleData_WebService.Low = Math.Min(sameCandleData_WebService.Low, candleData.Low);
|
|
||||||
sameCandleData_WebService.High = Math.Max(sameCandleData_WebService.High, candleData.High);
|
|
||||||
|
|
||||||
sameCandleData_WebService.WebServiceUpdate = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
candleDataList.Add(candleData);
|
|
||||||
candleData.WebServiceUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
//var sameCandleData_WebService = candleDataList.Find(i => i.Argument.Minute == candleData.Argument.Minute &&
|
|
||||||
//i.Argument.Hour == candleData.Argument.Hour && i.WebSocketUpdate == true);
|
|
||||||
|
|
||||||
|
var sameCandleData_WebService = candleDataList.Find(i => candleData.Argument <= i.Argument &&
|
||||||
|
i.Argument < (candleData.Argument + timespan_add) && i.WebSocketUpdate == true);
|
||||||
|
|
||||||
// 기존 웹소켓으로 부터 받은 데이터에 수정 등록
|
|
||||||
if (sameCandleData_WebService != null)
|
if (sameCandleData_WebService != null)
|
||||||
{
|
{
|
||||||
|
// 종가 => 웹소켓 데이터 사용 (웹 서비스 호출 데이터 중 마지막 데이터는 캔들이 완성되지 않음)
|
||||||
|
// 시가 => 웹 서비스 데이터 사용
|
||||||
|
sameCandleData_WebService.Open = candleData.Open;
|
||||||
|
// 저가, 고가 => 비교해서 사용
|
||||||
|
sameCandleData_WebService.Low = Math.Min(sameCandleData_WebService.Low, candleData.Low);
|
||||||
|
sameCandleData_WebService.High = Math.Max(sameCandleData_WebService.High, candleData.High);
|
||||||
|
|
||||||
}*/
|
sameCandleData_WebService.WebServiceUpdate = true;
|
||||||
// 웹 서비스로 받은 데이터를 새로 추가
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
candleDataList.Add(candleData);
|
candleDataList.Add(candleData);
|
||||||
@@ -267,46 +247,60 @@ namespace BitgetAPI.WebService
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
//var sameCandleData_WebService = candleDataList.Find(i => i.Argument.Minute == candleData.Argument.Minute &&
|
||||||
|
//i.Argument.Hour == candleData.Argument.Hour && i.WebSocketUpdate == true);
|
||||||
|
|
||||||
|
|
||||||
|
// 기존 웹소켓으로 부터 받은 데이터에 수정 등록
|
||||||
|
if (sameCandleData_WebService != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
}*/
|
||||||
|
// 웹 서비스로 받은 데이터를 새로 추가
|
||||||
|
else
|
||||||
|
{
|
||||||
|
candleDataList.Add(candleData);
|
||||||
|
candleData.WebServiceUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case ExchangeWebReqType.BitgetAccountList_Future:
|
break;
|
||||||
{
|
case ExchangeWebReqType.BitgetAccountList_Future:
|
||||||
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
return JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
}
|
return JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
||||||
case ExchangeWebReqType.BitgetOrder_Future:
|
}
|
||||||
{
|
case ExchangeWebReqType.BitgetOrder_Future:
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
}
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
case ExchangeWebReqType.BitgetCancelOrder_Future:
|
}
|
||||||
{
|
case ExchangeWebReqType.BitgetCancelOrder_Future:
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
}
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
case ExchangeWebReqType.BitgetChangeLeverage_Future:
|
}
|
||||||
{
|
case ExchangeWebReqType.BitgetChangeLeverage_Future:
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
}
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
case ExchangeWebReqType.BitgetChangeMarginMode_Future:
|
}
|
||||||
{
|
case ExchangeWebReqType.BitgetChangeMarginMode_Future:
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
}
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
case ExchangeWebReqType.BitgetChangePositionMode_Future:
|
}
|
||||||
{
|
case ExchangeWebReqType.BitgetChangePositionMode_Future:
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
}
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
case ExchangeWebReqType.BitgetOrderDetail_Future:
|
}
|
||||||
{
|
case ExchangeWebReqType.BitgetOrderDetail_Future:
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
}
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
case ExchangeWebReqType.BitgetGetTimeStamp:
|
}
|
||||||
{
|
case ExchangeWebReqType.BitgetGetTimeStamp:
|
||||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
{
|
||||||
}
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
#endregion
|
}
|
||||||
}
|
#endregion
|
||||||
}catch(Exception e)
|
|
||||||
{
|
|
||||||
return returnJsonData;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1056,7 +1050,7 @@ namespace BitgetAPI.WebService
|
|||||||
candleDataList.Add(candleData);
|
candleDataList.Add(candleData);
|
||||||
|
|
||||||
// 기존 데이터 삭제
|
// 기존 데이터 삭제
|
||||||
if (candleDataList.Count > 1000)
|
if (candleDataList.Count > 500)
|
||||||
{
|
{
|
||||||
// 가장 오래된 데이터 삭제
|
// 가장 오래된 데이터 삭제
|
||||||
candleDataList.Remove(j => j.Argument == candleDataList.Min(i => i.Argument));
|
candleDataList.Remove(j => j.Argument == candleDataList.Min(i => i.Argument));
|
||||||
@@ -1113,7 +1107,6 @@ namespace BitgetAPI.WebService
|
|||||||
foreach (var min in timeList)
|
foreach (var min in timeList)
|
||||||
{
|
{
|
||||||
var minDataList = temp.Where(t => t.reciveTime.Minute == min).OrderBy(t => t.reciveTime).ToList();
|
var minDataList = temp.Where(t => t.reciveTime.Minute == min).OrderBy(t => t.reciveTime).ToList();
|
||||||
|
|
||||||
var lastData = minDataList[minDataList.Count - 1];
|
var lastData = minDataList[minDataList.Count - 1];
|
||||||
var candleData = new CandleData();
|
var candleData = new CandleData();
|
||||||
|
|
||||||
@@ -1157,7 +1150,7 @@ namespace BitgetAPI.WebService
|
|||||||
{
|
{
|
||||||
var max = candleDataList.Max(t => t.Argument);
|
var max = candleDataList.Max(t => t.Argument);
|
||||||
lastCandleData = candleDataList.Find(t => t.Argument == max);
|
lastCandleData = candleDataList.Find(t => t.Argument == max);
|
||||||
|
|
||||||
var flag = false;
|
var flag = false;
|
||||||
if (((int)mCandleTimeType) < 60)
|
if (((int)mCandleTimeType) < 60)
|
||||||
{
|
{
|
||||||
@@ -1245,8 +1238,7 @@ namespace BitgetAPI.WebService
|
|||||||
{
|
{
|
||||||
candleData.WebSocketUpdate = true;
|
candleData.WebSocketUpdate = true;
|
||||||
lastCandleData.Close = candleData.Open;
|
lastCandleData.Close = candleData.Open;
|
||||||
|
if(lastCandleData.High < lastCandleData.Close)
|
||||||
if (lastCandleData.High < lastCandleData.Close)
|
|
||||||
{
|
{
|
||||||
lastCandleData.High = lastCandleData.Close;
|
lastCandleData.High = lastCandleData.Close;
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1253,8 @@ namespace BitgetAPI.WebService
|
|||||||
if((int)mCandleTimeType > 60)
|
if((int)mCandleTimeType > 60)
|
||||||
{
|
{
|
||||||
dt = dt.AddMinutes(dt.Minute * -1);
|
dt = dt.AddMinutes(dt.Minute * -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 새 데이터 등록
|
// 새 데이터 등록
|
||||||
candleDataList.Add(candleData);
|
candleDataList.Add(candleData);
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version = "1.0" encoding = "UTF-8" ?>
|
|
||||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
xmlns:local="clr-namespace:Bitget_App"
|
|
||||||
x:Class="Bitget_App.App">
|
|
||||||
<Application.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
|
||||||
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
|
||||||
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
public partial class App : Application
|
|
||||||
{
|
|
||||||
public App()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
MainPage = new AppShell();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<Shell
|
|
||||||
x:Class="Bitget_App.AppShell"
|
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
xmlns:local="clr-namespace:Bitget_App"
|
|
||||||
Shell.FlyoutBehavior="Disabled"
|
|
||||||
Title="Bitget_App">
|
|
||||||
|
|
||||||
<ShellContent
|
|
||||||
Title="Home"
|
|
||||||
ContentTemplate="{DataTemplate local:MainPage}"
|
|
||||||
Route="MainPage" />
|
|
||||||
|
|
||||||
</Shell>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
public partial class AppShell : Shell
|
|
||||||
{
|
|
||||||
public AppShell()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
|
|
||||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>
|
|
||||||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
|
|
||||||
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
|
|
||||||
|
|
||||||
<!-- Note for MacCatalyst:
|
|
||||||
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
|
|
||||||
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
|
|
||||||
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
|
|
||||||
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
|
|
||||||
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
|
|
||||||
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<RootNamespace>Bitget_App</RootNamespace>
|
|
||||||
<UseMaui>true</UseMaui>
|
|
||||||
<SingleProject>true</SingleProject>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<!-- Display name -->
|
|
||||||
<ApplicationTitle>Bitget_App</ApplicationTitle>
|
|
||||||
|
|
||||||
<!-- App Identifier -->
|
|
||||||
<ApplicationId>com.companyname.bitget_app</ApplicationId>
|
|
||||||
|
|
||||||
<!-- Versions -->
|
|
||||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
|
||||||
<ApplicationVersion>1</ApplicationVersion>
|
|
||||||
|
|
||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
|
|
||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
|
|
||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<!-- App Icon -->
|
|
||||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
|
||||||
|
|
||||||
<!-- Splash Screen -->
|
|
||||||
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
|
|
||||||
|
|
||||||
<!-- Images -->
|
|
||||||
<MauiImage Include="Resources\Images\*" />
|
|
||||||
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
|
|
||||||
|
|
||||||
<!-- Custom Fonts -->
|
|
||||||
<MauiFont Include="Resources\Fonts\*" />
|
|
||||||
|
|
||||||
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
|
|
||||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AndroidResource Remove="Platforms\Android\Resources\values\strings.xml" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="google-services.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<GoogleServicesJson Include="google-services.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
|
||||||
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Plugin.FirebaseCrashlytics" Version="4.1.0" />
|
|
||||||
<PackageReference Include="Plugin.LocalNotification" Version="10.1.8" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
|
||||||
<IsFirstTimeProjectOpen>False</IsFirstTimeProjectOpen>
|
|
||||||
<ActiveDebugFramework>net8.0-android</ActiveDebugFramework>
|
|
||||||
<ActiveDebugProfile>Xiaomi 23049PCD8G(Android 13.0 - API 33)</ActiveDebugProfile>
|
|
||||||
<SelectedPlatformGroup>PhysicalDevice</SelectedPlatformGroup>
|
|
||||||
<DefaultDevice>pixel_5_-_api_34</DefaultDevice>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="App.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
<None Update="AppShell.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
<None Update="MainPage.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
<None Update="Platforms\Windows\App.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
<None Update="Platforms\Windows\Package.appxmanifest">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
<None Update="Resources\Styles\Colors.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
<None Update="Resources\Styles\Styles.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Bitget_App.MainPage">
|
|
||||||
|
|
||||||
<ScrollView>
|
|
||||||
<VerticalStackLayout
|
|
||||||
Padding="30,0"
|
|
||||||
Spacing="25">
|
|
||||||
<Image
|
|
||||||
Source="dotnet_bot.png"
|
|
||||||
HeightRequest="185"
|
|
||||||
Aspect="AspectFit"
|
|
||||||
SemanticProperties.Description="dot net bot in a race car number eight" />
|
|
||||||
|
|
||||||
<Label
|
|
||||||
Text="Hello, World!"
|
|
||||||
Style="{StaticResource Headline}"
|
|
||||||
SemanticProperties.HeadingLevel="Level1" />
|
|
||||||
|
|
||||||
<Label
|
|
||||||
Text="Welcome to .NET Multi-platform App UI"
|
|
||||||
Style="{StaticResource SubHeadline}"
|
|
||||||
SemanticProperties.HeadingLevel="Level2"
|
|
||||||
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
x:Name="CounterBtn"
|
|
||||||
Text="Click me"
|
|
||||||
SemanticProperties.Hint="Counts the number of times you click"
|
|
||||||
Clicked="OnCounterClicked"
|
|
||||||
HorizontalOptions="Fill" />
|
|
||||||
</VerticalStackLayout>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</ContentPage>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
using Plugin.LocalNotification;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
public partial class MainPage : ContentPage
|
|
||||||
{
|
|
||||||
|
|
||||||
public MainPage()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
Permissions.RequestAsync<Permissions.PostNotifications>();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCounterClicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var request = new NotificationRequest
|
|
||||||
{
|
|
||||||
NotificationId = 1000,
|
|
||||||
Title = "MEDIUM",
|
|
||||||
Subtitle = "Hello! I'm Erdal",
|
|
||||||
Description = "Local Push Notification",
|
|
||||||
BadgeNumber = 42
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalNotificationCenter.Current.Show(request);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Maui.LifecycleEvents;
|
|
||||||
using Plugin.LocalNotification;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
public static class MauiProgram
|
|
||||||
{
|
|
||||||
public static MauiApp CreateMauiApp()
|
|
||||||
{
|
|
||||||
var builder = MauiApp.CreateBuilder();
|
|
||||||
builder
|
|
||||||
.UseMauiApp<App>()
|
|
||||||
.UseLocalNotification()
|
|
||||||
.ConfigureFonts(fonts =>
|
|
||||||
{
|
|
||||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
|
||||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
|
||||||
});
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
builder.Logging.AddDebug();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return builder.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MauiAppBuilder RegisterFirebaseServices(this MauiAppBuilder builder)
|
|
||||||
{
|
|
||||||
builder.ConfigureLifecycleEvents(events =>
|
|
||||||
{
|
|
||||||
events.AddAndroid(android => android.OnCreate((activity, _) =>
|
|
||||||
CrossFirebase.Initialize(activity, CreateCrossFirebaseSettings())));
|
|
||||||
CrossFirebaseCrashlytics.Current.SetCrashlyticsCollectionEnabled(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddSingleton(_ => CrossFirebaseAuth.Current);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CrossFirebaseSettings CreateCrossFirebaseSettings()
|
|
||||||
{
|
|
||||||
return new CrossFirebaseSettings(isAuthEnabled: true,
|
|
||||||
isCloudMessagingEnabled: true, isAnalyticsEnabled: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.million.pushalarmbitget">
|
|
||||||
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true" android:label="PushAlarmBitget">
|
|
||||||
|
|
||||||
</application>
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
|
||||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
|
||||||
</manifest>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Android.App;
|
|
||||||
using Android.Content.PM;
|
|
||||||
using Android.OS;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
|
||||||
public class MainActivity : MauiAppCompatActivity
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using Android.App;
|
|
||||||
using Android.Runtime;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
[Application]
|
|
||||||
public class MainApplication : MauiApplication
|
|
||||||
{
|
|
||||||
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
|
|
||||||
: base(handle, ownership)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="colorPrimary">#512BD4</color>
|
|
||||||
<color name="colorPrimaryDark">#2B0B98</color>
|
|
||||||
<color name="colorAccent">#2B0B98</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<resources>
|
|
||||||
<string name="com.google.firebase.crashlytics.mapping_file_id">none</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Foundation;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
[Register("AppDelegate")]
|
|
||||||
public class AppDelegate : MauiUIApplicationDelegate
|
|
||||||
{
|
|
||||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
|
|
||||||
<dict>
|
|
||||||
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<!-- The Mac App Store requires you specify if the app uses encryption. -->
|
|
||||||
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
|
|
||||||
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
|
|
||||||
<!-- Please indicate <true/> or <false/> here. -->
|
|
||||||
|
|
||||||
<!-- Specify the category for your app here. -->
|
|
||||||
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
|
|
||||||
<!-- <key>LSApplicationCategoryType</key> -->
|
|
||||||
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
|
|
||||||
<key>UIDeviceFamily</key>
|
|
||||||
<array>
|
|
||||||
<integer>2</integer>
|
|
||||||
</array>
|
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
|
||||||
<array>
|
|
||||||
<string>arm64</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>XSAppIconAssets</key>
|
|
||||||
<string>Assets.xcassets/appicon.appiconset</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using ObjCRuntime;
|
|
||||||
using UIKit;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
// This is the main entry point of the application.
|
|
||||||
static void Main(string[] args)
|
|
||||||
{
|
|
||||||
// if you want to use a different Application Delegate class from "AppDelegate"
|
|
||||||
// you can specify it here.
|
|
||||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using Microsoft.Maui;
|
|
||||||
using Microsoft.Maui.Hosting;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
internal class Program : MauiApplication
|
|
||||||
{
|
|
||||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
|
||||||
|
|
||||||
static void Main(string[] args)
|
|
||||||
{
|
|
||||||
var app = new Program();
|
|
||||||
app.Run(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
|
|
||||||
<profile name="common" />
|
|
||||||
<ui-application appid="maui-application-id-placeholder" exec="Bitget_App.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
|
||||||
<label>maui-application-title-placeholder</label>
|
|
||||||
<icon>maui-appicon-placeholder</icon>
|
|
||||||
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
|
|
||||||
</ui-application>
|
|
||||||
<shortcut-list />
|
|
||||||
<privileges>
|
|
||||||
<privilege>http://tizen.org/privilege/internet</privilege>
|
|
||||||
</privileges>
|
|
||||||
<dependencies />
|
|
||||||
<provides-appdefined-privileges />
|
|
||||||
</manifest>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<maui:MauiWinUIApplication
|
|
||||||
x:Class="Bitget_App.WinUI.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:maui="using:Microsoft.Maui"
|
|
||||||
xmlns:local="using:Bitget_App.WinUI">
|
|
||||||
|
|
||||||
</maui:MauiWinUIApplication>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
|
|
||||||
// To learn more about WinUI, the WinUI project structure,
|
|
||||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
|
||||||
|
|
||||||
namespace Bitget_App.WinUI
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides application-specific behavior to supplement the default Application class.
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : MauiWinUIApplication
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes the singleton application object. This is the first line of authored code
|
|
||||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
|
||||||
/// </summary>
|
|
||||||
public App()
|
|
||||||
{
|
|
||||||
this.InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Package
|
|
||||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
|
||||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
|
||||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
|
||||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
|
||||||
IgnorableNamespaces="uap rescap">
|
|
||||||
|
|
||||||
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
|
|
||||||
|
|
||||||
<mp:PhoneIdentity PhoneProductId="FAECAA4B-8E01-410F-BCD4-E9EA250D8C26" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
|
||||||
|
|
||||||
<Properties>
|
|
||||||
<DisplayName>$placeholder$</DisplayName>
|
|
||||||
<PublisherDisplayName>User Name</PublisherDisplayName>
|
|
||||||
<Logo>$placeholder$.png</Logo>
|
|
||||||
</Properties>
|
|
||||||
|
|
||||||
<Dependencies>
|
|
||||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
|
||||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
|
||||||
</Dependencies>
|
|
||||||
|
|
||||||
<Resources>
|
|
||||||
<Resource Language="x-generate" />
|
|
||||||
</Resources>
|
|
||||||
|
|
||||||
<Applications>
|
|
||||||
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
|
|
||||||
<uap:VisualElements
|
|
||||||
DisplayName="$placeholder$"
|
|
||||||
Description="$placeholder$"
|
|
||||||
Square150x150Logo="$placeholder$.png"
|
|
||||||
Square44x44Logo="$placeholder$.png"
|
|
||||||
BackgroundColor="transparent">
|
|
||||||
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
|
|
||||||
<uap:SplashScreen Image="$placeholder$.png" />
|
|
||||||
</uap:VisualElements>
|
|
||||||
</Application>
|
|
||||||
</Applications>
|
|
||||||
|
|
||||||
<Capabilities>
|
|
||||||
<rescap:Capability Name="runFullTrust" />
|
|
||||||
</Capabilities>
|
|
||||||
|
|
||||||
</Package>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
|
||||||
<assemblyIdentity version="1.0.0.0" name="Bitget_App.WinUI.app"/>
|
|
||||||
|
|
||||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<windowsSettings>
|
|
||||||
<!-- The combination of below two tags have the following effect:
|
|
||||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
|
||||||
2) System < Windows 10 Anniversary Update
|
|
||||||
-->
|
|
||||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
|
||||||
</windowsSettings>
|
|
||||||
</application>
|
|
||||||
</assembly>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using Foundation;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
[Register("AppDelegate")]
|
|
||||||
public class AppDelegate : MauiUIApplicationDelegate
|
|
||||||
{
|
|
||||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIDeviceFamily</key>
|
|
||||||
<array>
|
|
||||||
<integer>1</integer>
|
|
||||||
<integer>2</integer>
|
|
||||||
</array>
|
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
|
||||||
<array>
|
|
||||||
<string>arm64</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>XSAppIconAssets</key>
|
|
||||||
<string>Assets.xcassets/appicon.appiconset</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using ObjCRuntime;
|
|
||||||
using UIKit;
|
|
||||||
|
|
||||||
namespace Bitget_App
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
// This is the main entry point of the application.
|
|
||||||
static void Main(string[] args)
|
|
||||||
{
|
|
||||||
// if you want to use a different Application Delegate class from "AppDelegate"
|
|
||||||
// you can specify it here.
|
|
||||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"Windows Machine": {
|
|
||||||
"commandName": "MsixPackage",
|
|
||||||
"nativeDebugging": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 228 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB |
@@ -1,15 +0,0 @@
|
|||||||
Any raw assets you want to be deployed with your application can be placed in
|
|
||||||
this directory (and child directories). Deployment of the asset to your application
|
|
||||||
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
|
|
||||||
|
|
||||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
|
||||||
|
|
||||||
These files will be deployed with your package and will be accessible using Essentials:
|
|
||||||
|
|
||||||
async Task LoadMauiAsset()
|
|
||||||
{
|
|
||||||
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
|
|
||||||
var contents = reader.ReadToEnd();
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<?xaml-comp compile="true" ?>
|
|
||||||
<ResourceDictionary
|
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
|
||||||
|
|
||||||
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
|
|
||||||
|
|
||||||
<Color x:Key="Primary">#512BD4</Color>
|
|
||||||
<Color x:Key="PrimaryDark">#ac99ea</Color>
|
|
||||||
<Color x:Key="PrimaryDarkText">#242424</Color>
|
|
||||||
<Color x:Key="Secondary">#DFD8F7</Color>
|
|
||||||
<Color x:Key="SecondaryDarkText">#9880e5</Color>
|
|
||||||
<Color x:Key="Tertiary">#2B0B98</Color>
|
|
||||||
|
|
||||||
<Color x:Key="White">White</Color>
|
|
||||||
<Color x:Key="Black">Black</Color>
|
|
||||||
<Color x:Key="Magenta">#D600AA</Color>
|
|
||||||
<Color x:Key="MidnightBlue">#190649</Color>
|
|
||||||
<Color x:Key="OffBlack">#1f1f1f</Color>
|
|
||||||
|
|
||||||
<Color x:Key="Gray100">#E1E1E1</Color>
|
|
||||||
<Color x:Key="Gray200">#C8C8C8</Color>
|
|
||||||
<Color x:Key="Gray300">#ACACAC</Color>
|
|
||||||
<Color x:Key="Gray400">#919191</Color>
|
|
||||||
<Color x:Key="Gray500">#6E6E6E</Color>
|
|
||||||
<Color x:Key="Gray600">#404040</Color>
|
|
||||||
<Color x:Key="Gray900">#212121</Color>
|
|
||||||
<Color x:Key="Gray950">#141414</Color>
|
|
||||||
|
|
||||||
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
|
|
||||||
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
|
|
||||||
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
|
|
||||||
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
|
|
||||||
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
|
|
||||||
|
|
||||||
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
|
|
||||||
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
|
|
||||||
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
|
|
||||||
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
|
|
||||||
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
|
|
||||||
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
|
|
||||||
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
|
|
||||||
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
|
|
||||||
</ResourceDictionary>
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<?xaml-comp compile="true" ?>
|
|
||||||
<ResourceDictionary
|
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
|
||||||
|
|
||||||
<Style TargetType="ActivityIndicator">
|
|
||||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="IndicatorView">
|
|
||||||
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
|
|
||||||
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Border">
|
|
||||||
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
|
||||||
<Setter Property="StrokeShape" Value="Rectangle"/>
|
|
||||||
<Setter Property="StrokeThickness" Value="1"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="BoxView">
|
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Button">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
|
||||||
<Setter Property="FontSize" Value="14"/>
|
|
||||||
<Setter Property="BorderWidth" Value="0"/>
|
|
||||||
<Setter Property="CornerRadius" Value="8"/>
|
|
||||||
<Setter Property="Padding" Value="14,10"/>
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
<VisualState x:Name="PointerOver" />
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="CheckBox">
|
|
||||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="DatePicker">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
|
||||||
<Setter Property="FontSize" Value="14"/>
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Editor">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
|
||||||
<Setter Property="FontSize" Value="14" />
|
|
||||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Entry">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
|
||||||
<Setter Property="FontSize" Value="14" />
|
|
||||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Frame">
|
|
||||||
<Setter Property="HasShadow" Value="False" />
|
|
||||||
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
|
||||||
<Setter Property="CornerRadius" Value="8" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="ImageButton">
|
|
||||||
<Setter Property="Opacity" Value="1" />
|
|
||||||
<Setter Property="BorderColor" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderWidth" Value="0"/>
|
|
||||||
<Setter Property="CornerRadius" Value="0"/>
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="Opacity" Value="0.5" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
<VisualState x:Name="PointerOver" />
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Label">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
|
||||||
<Setter Property="FontSize" Value="14" />
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Span">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Label" x:Key="Headline">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="FontSize" Value="32" />
|
|
||||||
<Setter Property="HorizontalOptions" Value="Center" />
|
|
||||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Label" x:Key="SubHeadline">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="FontSize" Value="24" />
|
|
||||||
<Setter Property="HorizontalOptions" Value="Center" />
|
|
||||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="ListView">
|
|
||||||
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
|
||||||
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Picker">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
|
||||||
<Setter Property="FontSize" Value="14"/>
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="ProgressBar">
|
|
||||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="RadioButton">
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
|
||||||
<Setter Property="FontSize" Value="14"/>
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="RefreshView">
|
|
||||||
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="SearchBar">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
|
||||||
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
|
||||||
<Setter Property="FontSize" Value="14" />
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="SearchHandler">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
|
||||||
<Setter Property="FontSize" Value="14" />
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Shadow">
|
|
||||||
<Setter Property="Radius" Value="15" />
|
|
||||||
<Setter Property="Opacity" Value="0.5" />
|
|
||||||
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="Offset" Value="10,10" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Slider">
|
|
||||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
|
||||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
|
||||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
|
||||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="SwipeItem">
|
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Switch">
|
|
||||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="ThumbColor" Value="{StaticResource White}" />
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
<VisualState x:Name="On">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
|
|
||||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
<VisualState x:Name="Off">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="TimePicker">
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
|
||||||
<Setter Property="FontSize" Value="14"/>
|
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
|
||||||
<VisualStateGroupList>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Normal" />
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateGroupList>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Page" ApplyToDerivedTypes="True">
|
|
||||||
<Setter Property="Padding" Value="0"/>
|
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Shell" ApplyToDerivedTypes="True">
|
|
||||||
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
|
||||||
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
|
||||||
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
|
||||||
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
|
||||||
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
|
|
||||||
<Setter Property="Shell.NavBarHasShadow" Value="False" />
|
|
||||||
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
|
||||||
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="NavigationPage">
|
|
||||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
|
||||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="TabbedPage">
|
|
||||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
|
|
||||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
|
||||||
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
|
||||||
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
</ResourceDictionary>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "362142360131",
|
|
||||||
"project_id": "pushalarmbitget",
|
|
||||||
"storage_bucket": "pushalarmbitget.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:362142360131:android:e4b0c5f82de8be9937eaf6",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "com.million.pushalarmbitget"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAl52uVL19axoWuGh_YD5C5vvrTp_fnuV4"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
51
Bitget_Server/.dockerignore
Normal file
51
Bitget_Server/.dockerignore
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# .dockerignore — Bitget_Server
|
||||||
|
# Build context 가 솔루션 루트라도 동일하게 동작하도록 작성
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 빌드 산출물
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
**/out/
|
||||||
|
**/publish/
|
||||||
|
|
||||||
|
# Visual Studio / IDE
|
||||||
|
**/.vs/
|
||||||
|
**/.idea/
|
||||||
|
**/.vscode/
|
||||||
|
**/*.user
|
||||||
|
**/*.suo
|
||||||
|
**/*.userprefs
|
||||||
|
|
||||||
|
# 버전 관리
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# 패키지 / 캐시
|
||||||
|
**/packages/
|
||||||
|
**/node_modules/
|
||||||
|
**/.nuget/
|
||||||
|
|
||||||
|
# 로그/데이터 (호스트 마운트로 주입)
|
||||||
|
**/data/
|
||||||
|
**/logs/
|
||||||
|
**/*.log
|
||||||
|
|
||||||
|
# 문서/메타
|
||||||
|
*.md
|
||||||
|
LICENSE
|
||||||
|
**/.editorconfig
|
||||||
|
|
||||||
|
# 개발 전용 설정 / 시크릿
|
||||||
|
**/appsettings.Development.json
|
||||||
|
**/launchSettings.json
|
||||||
|
**/secrets.json
|
||||||
|
|
||||||
|
# 기존 WinForms 프로젝트 (이식 대상 아님 — 컨텍스트만 가벼워지면 됨)
|
||||||
|
BitgetApi/
|
||||||
|
|
||||||
|
# Docker 자체 메타
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*.yml
|
||||||
47
Bitget_Server/Api/Controllers/CoinsController.cs
Normal file
47
Bitget_Server/Api/Controllers/CoinsController.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Bitget_Server.Application.Services;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms 코인 선택 그리드 데이터를 REST 화.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/coins")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class CoinsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly SettingsService _settings;
|
||||||
|
private readonly ILogger<CoinsController> _logger;
|
||||||
|
|
||||||
|
public CoinsController(SettingsService settings, ILogger<CoinsController> logger)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// USDT 기준 Bitget 선물 마켓 정보 목록을 반환한다.
|
||||||
|
/// 내부적으로 BitgetClient 캐시(_state.marketInfoList)를 우선 사용하고 비어 있으면 호출.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<MarketInfo>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
|
||||||
|
public async Task<ActionResult<IEnumerable<MarketInfo>>> GetCoins(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var coins = await _settings.GetCoinsAsync(ct);
|
||||||
|
return Ok(coins);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "코인 조회 실패");
|
||||||
|
return Problem(
|
||||||
|
detail: ex.Message,
|
||||||
|
statusCode: StatusCodes.Status502BadGateway,
|
||||||
|
title: "Failed to fetch coins from Bitget");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
212
Bitget_Server/Api/Controllers/DiagnosticsController.cs
Normal file
212
Bitget_Server/Api/Controllers/DiagnosticsController.cs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
using Bitget_Server.Api.Dtos;
|
||||||
|
using Bitget_Server.Application.Services;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 런타임 캐시/지표 계산 결과를 외부에서 점검하기 위한 진단용 API.
|
||||||
|
/// wwwroot/chart.html 화면이 주로 소비한다.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/diag")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class DiagnosticsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly PriceLevelTracker _priceLevels;
|
||||||
|
private readonly SignalProcessService _signals;
|
||||||
|
|
||||||
|
public DiagnosticsController(MarketState state, PriceLevelTracker priceLevels, SignalProcessService signals)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_priceLevels = priceLevels;
|
||||||
|
_signals = signals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 캐시에 존재하는 심볼 + 서버가 지원하는 인터벌 목록.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("symbols")]
|
||||||
|
[ProducesResponseType(typeof(DiagSymbolsResponse), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<DiagSymbolsResponse> GetSymbols()
|
||||||
|
{
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
var symbols = _state.candleDataLists
|
||||||
|
.Select(x => x.ConGoodsData?.MarketName)
|
||||||
|
.Where(n => !string.IsNullOrEmpty(n))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var intervals = Enum.GetNames(typeof(MCandleTimeType));
|
||||||
|
return Ok(new DiagSymbolsResponse(symbols, intervals));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 특정 심볼/인터벌의 캔들 데이터 스냅샷 + 인디케이터 동봉.
|
||||||
|
/// Chart.js financial 플러그인 호환 포맷 (t,o,h,l,c).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("candles")]
|
||||||
|
[ProducesResponseType(typeof(DiagCandleResponse), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult<DiagCandleResponse> GetCandles(
|
||||||
|
[FromQuery] string symbol,
|
||||||
|
[FromQuery] string interval = "M1",
|
||||||
|
[FromQuery] int limit = 200)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(symbol))
|
||||||
|
return Problem("symbol 파라미터 필수", statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
if (!Enum.TryParse<MCandleTimeType>(interval, ignoreCase: true, out var t))
|
||||||
|
return Problem($"지원하지 않는 interval: {interval}", statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
List<CandleData> snapshot;
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
var entry = _state.candleDataLists
|
||||||
|
.FirstOrDefault(x => string.Equals(x.ConGoodsData?.MarketName, symbol, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (entry == null || entry.candleDataList == null || !entry.candleDataList.TryGetValue(t, out var list) || list == null)
|
||||||
|
return Problem(
|
||||||
|
detail: $"캐시에 {symbol}/{interval} 데이터가 없습니다. WebSocketCalcBackgroundService 가 동작 중이며 대상 마켓이 checked 되었는지 확인.",
|
||||||
|
statusCode: StatusCodes.Status404NotFound,
|
||||||
|
title: "Candle data not cached");
|
||||||
|
|
||||||
|
// 얕은 복사 (원본 컬렉션 수정과 응답 직렬화 동시 진행 방지)
|
||||||
|
snapshot = list.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit > 0 && snapshot.Count > limit)
|
||||||
|
snapshot = snapshot.Skip(snapshot.Count - limit).ToList();
|
||||||
|
|
||||||
|
var dtos = snapshot.Select(ToDto).OrderBy(d => d.t).ToList();
|
||||||
|
return Ok(new DiagCandleResponse(symbol.ToUpperInvariant(), t.ToString(), dtos.Count, dtos));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 특정 심볼/인터벌에 대한 PriceLevelTracker 의 전체 내부 상태.
|
||||||
|
/// (전고가/전저가/전전고가/전전저가/tempHighVal/tempLowVal 등)
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("price-levels")]
|
||||||
|
[ProducesResponseType(typeof(DiagPriceLevelsDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult<DiagPriceLevelsDto> GetPriceLevels(
|
||||||
|
[FromQuery] string symbol,
|
||||||
|
[FromQuery] string interval = "M1")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(symbol))
|
||||||
|
return Problem("symbol 파라미터 필수", statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
var key = symbol + interval;
|
||||||
|
if (!_priceLevels.ContainsKey(key))
|
||||||
|
return Problem(
|
||||||
|
detail: $"PriceLevelTracker 에 키가 없습니다 (key={key}). SignalProcessBackgroundService 가 해당 마켓을 처리한 적이 있는지 확인.",
|
||||||
|
statusCode: StatusCodes.Status404NotFound,
|
||||||
|
title: "Price levels not tracked");
|
||||||
|
|
||||||
|
var levels = _priceLevels.GetOrCreate(key);
|
||||||
|
// 값 복사
|
||||||
|
var copy = new Dictionary<string, double>(levels);
|
||||||
|
return Ok(new DiagPriceLevelsDto(symbol.ToUpperInvariant(), interval, key, copy));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 활성 신호를 심볼/인터벌로 필터링해 차트 마커용 포맷으로 반환.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("signal-markers")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<DiagSignalMarkerDto>), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<IEnumerable<DiagSignalMarkerDto>> GetSignalMarkers(
|
||||||
|
[FromQuery] string symbol,
|
||||||
|
[FromQuery] string interval = null)
|
||||||
|
{
|
||||||
|
IEnumerable<DiagSignalMarkerDto> Map(IEnumerable<SignalInfoData> src, string dir) =>
|
||||||
|
src.Select(s => new DiagSignalMarkerDto(
|
||||||
|
direction: dir,
|
||||||
|
symbol: s.MarketName,
|
||||||
|
interval: s.MCandleTimeType.ToString(),
|
||||||
|
t: new DateTimeOffset(s.SignalStart == default ? DateTime.UtcNow : s.SignalStart.ToUniversalTime())
|
||||||
|
.ToUnixTimeMilliseconds(),
|
||||||
|
price: s.Price,
|
||||||
|
rate: s.Rate,
|
||||||
|
indicatorPrice: s.IndicatorPrice,
|
||||||
|
indicatorRate: s.IndicatorRate,
|
||||||
|
description: s.Description ?? string.Empty));
|
||||||
|
|
||||||
|
bool SymbolMatch(SignalInfoData s) =>
|
||||||
|
string.IsNullOrWhiteSpace(symbol) ||
|
||||||
|
string.Equals(s.MarketName, symbol, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
bool IntervalMatch(SignalInfoData s) =>
|
||||||
|
string.IsNullOrWhiteSpace(interval) ||
|
||||||
|
string.Equals(s.MCandleTimeType.ToString(), interval, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var all = new List<DiagSignalMarkerDto>();
|
||||||
|
all.AddRange(Map(_signals.SignalsLongUp.Where(s => SymbolMatch(s) && IntervalMatch(s)), "LongUp"));
|
||||||
|
all.AddRange(Map(_signals.SignalsShortDown.Where(s => SymbolMatch(s) && IntervalMatch(s)), "ShortDown"));
|
||||||
|
all.AddRange(Map(_signals.SignalsLongDown.Where(s => SymbolMatch(s) && IntervalMatch(s)), "LongDown"));
|
||||||
|
all.AddRange(Map(_signals.SignalsShortUp.Where(s => SymbolMatch(s) && IntervalMatch(s)), "ShortUp"));
|
||||||
|
|
||||||
|
return Ok(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// private helpers
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private static DiagCandleDto ToDto(CandleData c)
|
||||||
|
{
|
||||||
|
long t = c.Argument == default
|
||||||
|
? 0L
|
||||||
|
: new DateTimeOffset(DateTime.SpecifyKind(c.Argument, DateTimeKind.Utc)).ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
DiagIchimokuDto ichi = null;
|
||||||
|
if (c.IchimokData != null)
|
||||||
|
{
|
||||||
|
ichi = new DiagIchimokuDto(
|
||||||
|
c.IchimokData.기준선,
|
||||||
|
c.IchimokData.전환선,
|
||||||
|
c.IchimokData.후행스팬,
|
||||||
|
c.IchimokData.선행스팬1,
|
||||||
|
c.IchimokData.선행스팬2,
|
||||||
|
c.IchimokData.상한선,
|
||||||
|
c.IchimokData.하한선,
|
||||||
|
c.IchimokData.IsLongUp,
|
||||||
|
c.IchimokData.IsLongDown,
|
||||||
|
c.IchimokData.IsShortUp,
|
||||||
|
c.IchimokData.IsShortDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<int, DiagMovingAverageDto> ma = null;
|
||||||
|
if (c.movingAverageDataList != null)
|
||||||
|
{
|
||||||
|
ma = c.movingAverageDataList.ToDictionary(
|
||||||
|
kv => kv.Key,
|
||||||
|
kv => new DiagMovingAverageDto(
|
||||||
|
kv.Value.Value,
|
||||||
|
kv.Value.Average,
|
||||||
|
kv.Value.BolangerValue_Up,
|
||||||
|
kv.Value.BolangerValue_Down));
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<int, DiagImpulseMacdDto> macd = null;
|
||||||
|
if (c.impulseMacdDataList != null)
|
||||||
|
{
|
||||||
|
macd = c.impulseMacdDataList.ToDictionary(
|
||||||
|
kv => kv.Key,
|
||||||
|
kv => new DiagImpulseMacdDto(kv.Value.MD, kv.Value.SH, kv.Value.SB));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DiagCandleDto(
|
||||||
|
t,
|
||||||
|
c.Open, c.High, c.Low, c.Close,
|
||||||
|
c.Vol_C, c.Vol_USDT,
|
||||||
|
c.WebSocketUpdate, c.WebServiceUpdate,
|
||||||
|
ichi, ma, macd);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Bitget_Server/Api/Controllers/HealthController.cs
Normal file
21
Bitget_Server/Api/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 단순 헬스체크. Docker / 로드밸런서에서 사용.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/health")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class HealthController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public IActionResult Get() => Ok(new
|
||||||
|
{
|
||||||
|
status = "ok",
|
||||||
|
timestampUtc = DateTime.UtcNow,
|
||||||
|
version = typeof(HealthController).Assembly.GetName().Version?.ToString() ?? "unknown"
|
||||||
|
});
|
||||||
|
}
|
||||||
148
Bitget_Server/Api/Controllers/SettingsController.cs
Normal file
148
Bitget_Server/Api/Controllers/SettingsController.cs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
using Bitget_Server.Api.Dtos;
|
||||||
|
using Bitget_Server.Application.Services;
|
||||||
|
using Bitget_Server.Infrastructure.Utils;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms MainForm 의 ComboBox / 텍스트박스 / API 선택 라디오버튼을 REST 화.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/settings")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class SettingsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly SettingsService _settings;
|
||||||
|
|
||||||
|
public SettingsController(SettingsService settings)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 캔들 간격 =====
|
||||||
|
|
||||||
|
/// <summary>현재 선택된 캔들 간격(분)을 반환.</summary>
|
||||||
|
[HttpGet("candle-interval")]
|
||||||
|
[ProducesResponseType(typeof(CandleIntervalDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<CandleIntervalDto> GetCandleInterval()
|
||||||
|
{
|
||||||
|
var minutes = _settings.GetCandleIntervalMinutes();
|
||||||
|
var interval = (MCandleTimeType)minutes;
|
||||||
|
return Ok(new CandleIntervalDto(minutes, Convertor.GetEnumDescription(interval)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>캔들 간격을 변경.</summary>
|
||||||
|
[HttpPut("candle-interval")]
|
||||||
|
[ProducesResponseType(typeof(CandleIntervalDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||||
|
public ActionResult<CandleIntervalDto> UpdateCandleInterval([FromBody] UpdateCandleIntervalRequest req)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_settings.SetCandleIntervalMinutes(req.Minutes);
|
||||||
|
var interval = (MCandleTimeType)req.Minutes;
|
||||||
|
return Ok(new CandleIntervalDto(req.Minutes, Convertor.GetEnumDescription(interval)));
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest, title: "Invalid candle interval");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>지원되는 캔들 간격 목록.</summary>
|
||||||
|
[HttpGet("candle-interval/options")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<object>), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<IEnumerable<object>> ListCandleIntervals()
|
||||||
|
=> Ok(_settings.ListCandleIntervals());
|
||||||
|
|
||||||
|
// ===== 선택 코인 =====
|
||||||
|
|
||||||
|
[HttpGet("selected-coin")]
|
||||||
|
[ProducesResponseType(typeof(SelectedCoinDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<SelectedCoinDto> GetSelectedCoin()
|
||||||
|
=> Ok(new SelectedCoinDto(_settings.GetSelectedCoin()));
|
||||||
|
|
||||||
|
[HttpPut("selected-coin")]
|
||||||
|
[ProducesResponseType(typeof(SelectedCoinDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||||
|
public ActionResult<SelectedCoinDto> UpdateSelectedCoin([FromBody] UpdateSelectedCoinRequest req)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_settings.SetSelectedCoin(req.MarketName);
|
||||||
|
return Ok(new SelectedCoinDto(req.MarketName));
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest, title: "Invalid coin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 레버리지 =====
|
||||||
|
|
||||||
|
[HttpGet("leverage")]
|
||||||
|
[ProducesResponseType(typeof(LeverageDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<LeverageDto> GetLeverage()
|
||||||
|
=> Ok(new LeverageDto(_settings.GetLeverage()));
|
||||||
|
|
||||||
|
[HttpPut("leverage")]
|
||||||
|
[ProducesResponseType(typeof(LeverageDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||||
|
public ActionResult<LeverageDto> UpdateLeverage([FromBody] UpdateLeverageRequest req)
|
||||||
|
{
|
||||||
|
if (req.Leverage <= 0)
|
||||||
|
return Problem(detail: "Leverage must be > 0", statusCode: StatusCodes.Status400BadRequest, title: "Invalid leverage");
|
||||||
|
|
||||||
|
_settings.SetLeverage(req.Leverage);
|
||||||
|
return Ok(new LeverageDto(req.Leverage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 주문 수량 =====
|
||||||
|
|
||||||
|
[HttpGet("order-amount")]
|
||||||
|
[ProducesResponseType(typeof(OrderAmountDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<OrderAmountDto> GetOrderAmount()
|
||||||
|
=> Ok(new OrderAmountDto(_settings.GetOrderAmount()));
|
||||||
|
|
||||||
|
[HttpPut("order-amount")]
|
||||||
|
[ProducesResponseType(typeof(OrderAmountDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||||
|
public ActionResult<OrderAmountDto> UpdateOrderAmount([FromBody] UpdateOrderAmountRequest req)
|
||||||
|
{
|
||||||
|
if (req.OrderAmount <= 0)
|
||||||
|
return Problem(detail: "OrderAmount must be > 0", statusCode: StatusCodes.Status400BadRequest, title: "Invalid order amount");
|
||||||
|
|
||||||
|
_settings.SetOrderAmount(req.OrderAmount);
|
||||||
|
return Ok(new OrderAmountDto(req.OrderAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Selected API 키 =====
|
||||||
|
|
||||||
|
[HttpGet("api-key")]
|
||||||
|
[ProducesResponseType(typeof(SelectedApiDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public ActionResult<SelectedApiDto> GetSelectedApi()
|
||||||
|
{
|
||||||
|
var id = _settings.GetSelectedApiId();
|
||||||
|
if (id == null) return NoContent();
|
||||||
|
return Ok(new SelectedApiDto(id.Value, string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("api-key")]
|
||||||
|
[ProducesResponseType(typeof(SelectedApiDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult<SelectedApiDto> UpdateSelectedApi([FromBody] UpdateSelectedApiRequest req)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_settings.SetSelectedApiId(req.Id);
|
||||||
|
return Ok(new SelectedApiDto(req.Id, string.Empty));
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException ex)
|
||||||
|
{
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound, title: "API key not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
Bitget_Server/Api/Controllers/SignalsController.cs
Normal file
97
Bitget_Server/Api/Controllers/SignalsController.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using Bitget_Server.Api.Dtos;
|
||||||
|
using Bitget_Server.Application.Services;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms 의 구름떼/영호 조건식 체크박스 및 신호검색 그리드를 REST 화.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/signals")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class SignalsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly SignalProcessService _signalProcess;
|
||||||
|
private readonly MarketState _state;
|
||||||
|
|
||||||
|
public SignalsController(SignalProcessService signalProcess, MarketState state)
|
||||||
|
{
|
||||||
|
_signalProcess = signalProcess;
|
||||||
|
_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>현재 활성화된 시그널 목록 + 카운트.</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(SignalListResponse), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<SignalListResponse> GetSignals()
|
||||||
|
{
|
||||||
|
var result = new SignalListResponse(
|
||||||
|
_signalProcess.SignalsLongUp.Select(Map),
|
||||||
|
_signalProcess.SignalsShortDown.Select(Map),
|
||||||
|
_signalProcess.SignalsLongDown.Select(Map),
|
||||||
|
_signalProcess.SignalsShortUp.Select(Map),
|
||||||
|
_signalProcess.LongCount,
|
||||||
|
_signalProcess.ShortCount);
|
||||||
|
return Ok(result);
|
||||||
|
|
||||||
|
static SignalSummaryDto Map(Domain.Models.SignalInfoData s)
|
||||||
|
=> new(s.MarketName, s.Description, s.SignalStart, s.Price, s.Rate,
|
||||||
|
s.IndicatorPrice, s.IndicatorRate, s.MCandleTimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>조건식 상태 조회.</summary>
|
||||||
|
[HttpGet("conditions")]
|
||||||
|
[ProducesResponseType(typeof(SignalConditionsDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<SignalConditionsDto> GetConditions()
|
||||||
|
=> Ok(new SignalConditionsDto(
|
||||||
|
_signalProcess.Checked구름떼상향돌파,
|
||||||
|
_signalProcess.Checked구름떼하향돌파,
|
||||||
|
_signalProcess.영호조건식_상향돌파,
|
||||||
|
_signalProcess.영호조건식_하향돌파));
|
||||||
|
|
||||||
|
/// <summary>조건식 상태 변경.</summary>
|
||||||
|
[HttpPut("conditions")]
|
||||||
|
[ProducesResponseType(typeof(SignalConditionsDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<SignalConditionsDto> UpdateConditions([FromBody] UpdateSignalConditionsRequest req)
|
||||||
|
{
|
||||||
|
_signalProcess.Checked구름떼상향돌파 = req.구름떼상향돌파;
|
||||||
|
_signalProcess.Checked구름떼하향돌파 = req.구름떼하향돌파;
|
||||||
|
_signalProcess.영호조건식_상향돌파 = req.영호조건식_상향돌파;
|
||||||
|
_signalProcess.영호조건식_하향돌파 = req.영호조건식_하향돌파;
|
||||||
|
|
||||||
|
return Ok(new SignalConditionsDto(
|
||||||
|
_signalProcess.Checked구름떼상향돌파,
|
||||||
|
_signalProcess.Checked구름떼하향돌파,
|
||||||
|
_signalProcess.영호조건식_상향돌파,
|
||||||
|
_signalProcess.영호조건식_하향돌파));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>체크된 마켓/인터벌 목록 조회 (신호검색 대상).</summary>
|
||||||
|
[HttpGet("checked-markets")]
|
||||||
|
[ProducesResponseType(typeof(CheckedMarketsDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<CheckedMarketsDto> GetCheckedMarkets()
|
||||||
|
{
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
return Ok(new CheckedMarketsDto(
|
||||||
|
_state.CheckedMarkets.Select(m => m?.ToString() ?? string.Empty).ToList(),
|
||||||
|
_state.IntervalList.Select(i => i?.ToString() ?? string.Empty).ToList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>체크된 마켓/인터벌 일괄 설정.</summary>
|
||||||
|
[HttpPut("checked-markets")]
|
||||||
|
[ProducesResponseType(typeof(CheckedMarketsDto), StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<CheckedMarketsDto> UpdateCheckedMarkets([FromBody] UpdateCheckedMarketsRequest req)
|
||||||
|
{
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
_state.CheckedMarkets = req.Markets.Cast<object>().ToList();
|
||||||
|
_state.IntervalList = req.Intervals.Cast<object>().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new CheckedMarketsDto(req.Markets, req.Intervals));
|
||||||
|
}
|
||||||
|
}
|
||||||
122
Bitget_Server/Api/Controllers/TradingController.cs
Normal file
122
Bitget_Server/Api/Controllers/TradingController.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
using Bitget_Server.Api.Dtos;
|
||||||
|
using Bitget_Server.Application.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms 의 주문/취소/잔고 버튼을 REST 화.
|
||||||
|
/// Bitget Private API 를 호출하므로 반드시 선택된 API 키가 설정되어 있어야 한다.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/trading")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class TradingController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly BitgetTradingService _trading;
|
||||||
|
private readonly ILogger<TradingController> _logger;
|
||||||
|
|
||||||
|
public TradingController(BitgetTradingService trading, ILogger<TradingController> logger)
|
||||||
|
{
|
||||||
|
_trading = trading;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bitget 서버 타임스탬프 조회 (헬스체크 용도로도 활용).</summary>
|
||||||
|
[HttpGet("timestamp")]
|
||||||
|
[ProducesResponseType(typeof(long), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<long>> GetTimestamp(CancellationToken ct)
|
||||||
|
=> Ok(await _trading.GetTimeStampAsync(ct));
|
||||||
|
|
||||||
|
/// <summary>선물 계좌 잔고/포지션 조회.</summary>
|
||||||
|
[HttpGet("account")]
|
||||||
|
[ProducesResponseType(typeof(List<Dictionary<string, object>>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
|
||||||
|
public async Task<IActionResult> GetAccount(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _trading.GetFutureAccountListAsync(ct);
|
||||||
|
if (result == null)
|
||||||
|
return Problem(detail: "Bitget API 응답 없음", statusCode: StatusCodes.Status502BadGateway, title: "Account fetch failed");
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>미체결 주문 조회.</summary>
|
||||||
|
[HttpGet("orders/pending")]
|
||||||
|
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
|
||||||
|
public async Task<IActionResult> GetPendingOrders(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _trading.GetPendingOrdersAsync(ct);
|
||||||
|
if (result == null)
|
||||||
|
return Problem(detail: "Bitget API 응답 없음", statusCode: StatusCodes.Status502BadGateway, title: "Pending orders fetch failed");
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>주문 상세 조회.</summary>
|
||||||
|
[HttpGet("orders/{symbol}/{orderId}")]
|
||||||
|
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> GetOrderDetail(string symbol, string orderId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _trading.GetFutureOrderDetailAsync(symbol, orderId, ct);
|
||||||
|
if (result == null)
|
||||||
|
return Problem(detail: "주문 상세 응답 없음", statusCode: StatusCodes.Status404NotFound, title: "Order detail not found");
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>신규 주문.</summary>
|
||||||
|
[HttpPost("orders")]
|
||||||
|
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
|
||||||
|
public async Task<IActionResult> PlaceOrder([FromBody] PlaceOrderRequest req, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _trading.FutureOrderAsync(
|
||||||
|
req.Symbol,
|
||||||
|
req.MarginMode,
|
||||||
|
req.Size,
|
||||||
|
req.Price,
|
||||||
|
req.Side,
|
||||||
|
req.OrderType,
|
||||||
|
req.Leverage,
|
||||||
|
req.ClientOrderId ?? string.Empty,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
return Problem(detail: "주문 실패 (MarginMode/Leverage/Order 체인 중 단계 실패)",
|
||||||
|
statusCode: StatusCodes.Status502BadGateway, title: "Order failed");
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "주문 처리 실패: {Symbol}", req.Symbol);
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status500InternalServerError, title: "Order processing error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>주문 취소.</summary>
|
||||||
|
[HttpDelete("orders")]
|
||||||
|
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
|
||||||
|
public async Task<IActionResult> CancelOrder([FromBody] CancelOrderRequest req, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _trading.CancelFutureOrderAsync(req.Symbol, req.OrderId, ct);
|
||||||
|
if (result == null)
|
||||||
|
return Problem(detail: "취소 응답 없음", statusCode: StatusCodes.Status502BadGateway, title: "Cancel failed");
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>포지션 모드 변경(hedge / one_way).</summary>
|
||||||
|
[HttpPut("position-mode")]
|
||||||
|
[ProducesResponseType(typeof(Dictionary<string, object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status502BadGateway)]
|
||||||
|
public async Task<IActionResult> ChangePositionMode([FromBody] ChangePositionModeRequest req, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _trading.ChangeFuturePositionModeAsync(req.Mode, ct);
|
||||||
|
if (result == null)
|
||||||
|
return Problem(detail: "포지션 모드 변경 실패", statusCode: StatusCodes.Status502BadGateway, title: "Position mode change failed");
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
Bitget_Server/Api/Controllers/WebSocketController.cs
Normal file
66
Bitget_Server/Api/Controllers/WebSocketController.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Infrastructure.Bitget;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms 의 BitGetWebSocektStart / closeWebSocket 버튼 REST 화.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/websocket")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class WebSocketController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly BitgetWebSocketClient _ws;
|
||||||
|
private readonly ILogger<WebSocketController> _logger;
|
||||||
|
|
||||||
|
public WebSocketController(BitgetWebSocketClient ws, ILogger<WebSocketController> logger)
|
||||||
|
{
|
||||||
|
_ws = ws;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>현재 WebSocket 연결 상태.</summary>
|
||||||
|
[HttpGet("status")]
|
||||||
|
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||||
|
public IActionResult GetStatus() => Ok(new { connected = _ws.IsConnected });
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket 연결 시작. subscribeArgs 는 Bitget 공식 포맷의 args 리스트.
|
||||||
|
/// 예: [{"instType":"USDT-FUTURES","channel":"ticker","instId":"BTCUSDT"}]
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("start")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||||
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<IActionResult> Start(
|
||||||
|
[FromQuery] BitgetWebSocketType type,
|
||||||
|
[FromBody] IEnumerable<object> subscribeArgs,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dic = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "op", "subscribe" },
|
||||||
|
{ "args", subscribeArgs ?? Array.Empty<object>() }
|
||||||
|
};
|
||||||
|
await _ws.StartAsync(type, dic, ct);
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WebSocket Start 실패");
|
||||||
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status500InternalServerError, title: "WebSocket start failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>WebSocket 연결 종료.</summary>
|
||||||
|
[HttpPost("stop")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<IActionResult> Stop(CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _ws.StopAsync(ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Bitget_Server/Api/Dtos/DiagnosticsDtos.cs
Normal file
70
Bitget_Server/Api/Dtos/DiagnosticsDtos.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
namespace Bitget_Server.Api.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /api/diag 엔드포인트 전용 응답 모델.
|
||||||
|
/// Chart.js financial 플러그인 포맷에 맞춰 {t, o, h, l, c} 가 기본이며,
|
||||||
|
/// 인디케이터는 선택 필드로 포함된다.
|
||||||
|
/// </summary>
|
||||||
|
public record DiagSymbolsResponse(
|
||||||
|
IEnumerable<string> Symbols,
|
||||||
|
IEnumerable<string> Intervals);
|
||||||
|
|
||||||
|
public record DiagCandleDto(
|
||||||
|
long t, // epoch millis (Chart.js x-axis)
|
||||||
|
double o,
|
||||||
|
double h,
|
||||||
|
double l,
|
||||||
|
double c,
|
||||||
|
double volC,
|
||||||
|
double volUsdt,
|
||||||
|
bool wsUpdated,
|
||||||
|
bool restUpdated,
|
||||||
|
DiagIchimokuDto ichimoku,
|
||||||
|
Dictionary<int, DiagMovingAverageDto> ma,
|
||||||
|
Dictionary<int, DiagImpulseMacdDto> impulseMacd);
|
||||||
|
|
||||||
|
public record DiagIchimokuDto(
|
||||||
|
double 기준선,
|
||||||
|
double 전환선,
|
||||||
|
double 후행스팬,
|
||||||
|
double 선행스팬1,
|
||||||
|
double 선행스팬2,
|
||||||
|
double 상한선,
|
||||||
|
double 하한선,
|
||||||
|
bool IsLongUp,
|
||||||
|
bool IsLongDown,
|
||||||
|
bool IsShortUp,
|
||||||
|
bool IsShortDown);
|
||||||
|
|
||||||
|
public record DiagMovingAverageDto(
|
||||||
|
double Value,
|
||||||
|
double Average,
|
||||||
|
double BolUp,
|
||||||
|
double BolDown);
|
||||||
|
|
||||||
|
public record DiagImpulseMacdDto(double MD, double SH, double SB);
|
||||||
|
|
||||||
|
public record DiagCandleResponse(
|
||||||
|
string Symbol,
|
||||||
|
string Interval,
|
||||||
|
int Count,
|
||||||
|
IEnumerable<DiagCandleDto> Candles);
|
||||||
|
|
||||||
|
/// <summary>PriceLevelTracker 의 전고점/전저점 상태 전체 키·값.</summary>
|
||||||
|
public record DiagPriceLevelsDto(
|
||||||
|
string Symbol,
|
||||||
|
string Interval,
|
||||||
|
string Key,
|
||||||
|
IReadOnlyDictionary<string, double> Levels);
|
||||||
|
|
||||||
|
/// <summary>SignalProcessService 의 현재 신호 요약 (차트 마커용).</summary>
|
||||||
|
public record DiagSignalMarkerDto(
|
||||||
|
string direction, // "LongUp" / "ShortDown" / "LongDown" / "ShortUp"
|
||||||
|
string symbol,
|
||||||
|
string interval,
|
||||||
|
long t,
|
||||||
|
double price,
|
||||||
|
double rate,
|
||||||
|
double indicatorPrice,
|
||||||
|
double indicatorRate,
|
||||||
|
string description);
|
||||||
18
Bitget_Server/Api/Dtos/SettingsDtos.cs
Normal file
18
Bitget_Server/Api/Dtos/SettingsDtos.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Bitget_Server.Api.Dtos;
|
||||||
|
|
||||||
|
public record CandleIntervalDto(int Minutes, string Description);
|
||||||
|
|
||||||
|
public record UpdateCandleIntervalRequest(int Minutes);
|
||||||
|
|
||||||
|
public record SelectedCoinDto(string MarketName);
|
||||||
|
|
||||||
|
public record UpdateSelectedCoinRequest(string MarketName);
|
||||||
|
|
||||||
|
public record LeverageDto(double Leverage);
|
||||||
|
public record UpdateLeverageRequest(double Leverage);
|
||||||
|
|
||||||
|
public record OrderAmountDto(double OrderAmount);
|
||||||
|
public record UpdateOrderAmountRequest(double OrderAmount);
|
||||||
|
|
||||||
|
public record SelectedApiDto(int Id, string Name);
|
||||||
|
public record UpdateSelectedApiRequest(int Id);
|
||||||
37
Bitget_Server/Api/Dtos/SignalDtos.cs
Normal file
37
Bitget_Server/Api/Dtos/SignalDtos.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Dtos;
|
||||||
|
|
||||||
|
public record SignalConditionsDto(
|
||||||
|
bool 구름떼상향돌파,
|
||||||
|
bool 구름떼하향돌파,
|
||||||
|
bool 영호조건식_상향돌파,
|
||||||
|
bool 영호조건식_하향돌파);
|
||||||
|
|
||||||
|
public record UpdateSignalConditionsRequest(
|
||||||
|
bool 구름떼상향돌파,
|
||||||
|
bool 구름떼하향돌파,
|
||||||
|
bool 영호조건식_상향돌파,
|
||||||
|
bool 영호조건식_하향돌파);
|
||||||
|
|
||||||
|
public record CheckedMarketsDto(IEnumerable<string> Markets, IEnumerable<string> Intervals);
|
||||||
|
|
||||||
|
public record UpdateCheckedMarketsRequest(IEnumerable<string> Markets, IEnumerable<string> Intervals);
|
||||||
|
|
||||||
|
public record SignalListResponse(
|
||||||
|
IEnumerable<SignalSummaryDto> LongUp,
|
||||||
|
IEnumerable<SignalSummaryDto> ShortDown,
|
||||||
|
IEnumerable<SignalSummaryDto> LongDown,
|
||||||
|
IEnumerable<SignalSummaryDto> ShortUp,
|
||||||
|
int LongCount,
|
||||||
|
int ShortCount);
|
||||||
|
|
||||||
|
public record SignalSummaryDto(
|
||||||
|
string MarketName,
|
||||||
|
string Description,
|
||||||
|
DateTime SignalStart,
|
||||||
|
double Price,
|
||||||
|
double Rate,
|
||||||
|
double IndicatorPrice,
|
||||||
|
double IndicatorRate,
|
||||||
|
MCandleTimeType MCandleTimeType);
|
||||||
19
Bitget_Server/Api/Dtos/TradingDtos.cs
Normal file
19
Bitget_Server/Api/Dtos/TradingDtos.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Api.Dtos;
|
||||||
|
|
||||||
|
public record PlaceOrderRequest(
|
||||||
|
string Symbol,
|
||||||
|
FutureMarginMode MarginMode,
|
||||||
|
double Size,
|
||||||
|
double Price,
|
||||||
|
FutureSide Side,
|
||||||
|
OrderType OrderType,
|
||||||
|
double Leverage,
|
||||||
|
string ClientOrderId = "");
|
||||||
|
|
||||||
|
public record CancelOrderRequest(string Symbol, string OrderId);
|
||||||
|
|
||||||
|
public record ChangePositionModeRequest(FuturePositionMode Mode);
|
||||||
|
|
||||||
|
public record OrderDetailRequest(string Symbol, string OrderId);
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Bitget_Server.Application.Options;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Infrastructure.Bitget;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.BackgroundServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 MainForm.getMarketInfoList_WebService (300ms loop) 이식.
|
||||||
|
/// USDT-FUTURES 마켓 Ticker 정보를 주기적으로 새로고침하여 MarketState.marketInfoList 를 갱신한다.
|
||||||
|
/// </summary>
|
||||||
|
public class MarketInfoBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly BitgetClient _client;
|
||||||
|
private readonly BitgetOptions _options;
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly ILogger<MarketInfoBackgroundService> _logger;
|
||||||
|
|
||||||
|
public MarketInfoBackgroundService(
|
||||||
|
BitgetClient client,
|
||||||
|
IOptions<BitgetOptions> options,
|
||||||
|
MarketState state,
|
||||||
|
ILogger<MarketInfoBackgroundService> logger)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_options = options.Value;
|
||||||
|
_state = state;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("MarketInfoBackgroundService 시작 (Interval={Ms}ms)", _options.MarketInfoUpdateIntervalMs);
|
||||||
|
|
||||||
|
var delay = TimeSpan.FromMilliseconds(Math.Max(100, _options.MarketInfoUpdateIntervalMs));
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, stoppingToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetMarket_Future,
|
||||||
|
ct: stoppingToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 결과는 BitgetClient.Parse 가 _state.marketInfoList 에 반영함
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { /* shutdown */ }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "MarketInfo 업데이트 오류");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("MarketInfoBackgroundService 종료");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Bitget_Server.Application.Services;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.BackgroundServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 MainForm.Signalnfo → IndicatorProcess.calProcess 무한 루프 이식.
|
||||||
|
/// SignalProcessService.ProcessOnceAsync 를 짧은 주기로 반복 호출한다.
|
||||||
|
/// </summary>
|
||||||
|
public class SignalProcessBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly SignalProcessService _signalProcess;
|
||||||
|
private readonly ILogger<SignalProcessBackgroundService> _logger;
|
||||||
|
|
||||||
|
/// <summary>신호 검색 루프 1회 처리 후 대기 시간 (기존 while 루프는 Thread.Sleep 없이 반복되었으나 CPU 과점유 방지를 위해 200ms 부여).</summary>
|
||||||
|
private static readonly TimeSpan LoopInterval = TimeSpan.FromMilliseconds(200);
|
||||||
|
|
||||||
|
public SignalProcessBackgroundService(
|
||||||
|
SignalProcessService signalProcess,
|
||||||
|
ILogger<SignalProcessBackgroundService> logger)
|
||||||
|
{
|
||||||
|
_signalProcess = signalProcess;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("SignalProcessBackgroundService 시작 (Interval={Ms}ms)", LoopInterval.TotalMilliseconds);
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _signalProcess.ProcessOnceAsync(stoppingToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { /* shutdown */ }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "SignalProcess 1회 처리 오류");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(LoopInterval, stoppingToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { /* shutdown */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("SignalProcessBackgroundService 종료");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Bitget_Server.Application.Options;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Infrastructure.Bitget;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.BackgroundServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 MainForm.DataCalculateThread (1000ms loop) 이식.
|
||||||
|
/// BitgetWebSocketClient 가 수집한 메시지를 주기적으로 꺼내어 candleDataLists 로 병합한다.
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketCalcBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly BitgetWebSocketClient _ws;
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly BitgetOptions _options;
|
||||||
|
private readonly ILogger<WebSocketCalcBackgroundService> _logger;
|
||||||
|
|
||||||
|
public WebSocketCalcBackgroundService(
|
||||||
|
BitgetWebSocketClient ws,
|
||||||
|
MarketState state,
|
||||||
|
IOptions<BitgetOptions> options,
|
||||||
|
ILogger<WebSocketCalcBackgroundService> logger)
|
||||||
|
{
|
||||||
|
_ws = ws;
|
||||||
|
_state = state;
|
||||||
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("WebSocketCalcBackgroundService 시작 (Interval={Ms}ms)", _options.WebSocketCalcIntervalMs);
|
||||||
|
|
||||||
|
var delay = TimeSpan.FromMilliseconds(Math.Max(200, _options.WebSocketCalcIntervalMs));
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, stoppingToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// WebSocket 이 꺼져 있으면 처리할 데이터도 없으므로 skip
|
||||||
|
if (!_ws.IsConnected) continue;
|
||||||
|
|
||||||
|
List<object> intervalsSnapshot;
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
intervalsSnapshot = _state.IntervalList.ToList();
|
||||||
|
|
||||||
|
_ws.CalculateDataList(BitgetWebSocketType.캔들정보, intervalsSnapshot);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { /* shutdown */ }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "WebSocket 집계 루프 오류");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("WebSocketCalcBackgroundService 종료");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using Bitget_Server.Domain.Indicators;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Indicators;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class IchimokuConditionSetup
|
||||||
|
{
|
||||||
|
/// <summary>기준선 기간</summary>
|
||||||
|
public int BaselinePeriod { get; set; } = 26;
|
||||||
|
/// <summary>전환선 기간</summary>
|
||||||
|
public int ConversionPeriod { get; set; } = 9;
|
||||||
|
/// <summary>선행스팬2 기간</summary>
|
||||||
|
public int LaggingSpan2Period { get; set; } = 52;
|
||||||
|
/// <summary>Shift</summary>
|
||||||
|
public int Displacement { get; set; } = 26;
|
||||||
|
|
||||||
|
// 캔들당 일목균형표 계산하기
|
||||||
|
public void calIchimokuData(List<CandleData> candleDatas, CandleData candleData)
|
||||||
|
{
|
||||||
|
// 현재 종가 인덱스
|
||||||
|
var endIndex = candleDatas.Count() - 1;
|
||||||
|
|
||||||
|
if (candleData == null) return;
|
||||||
|
|
||||||
|
if (candleData.IchimokData == null) candleData.IchimokData = new IchimokData();
|
||||||
|
|
||||||
|
var endValue = candleData.Close;
|
||||||
|
|
||||||
|
// 전봉까지 만듬
|
||||||
|
var datas = candleDatas.FindAll(i =>
|
||||||
|
candleDatas.IndexOf(i) > endIndex - ConversionPeriod &&
|
||||||
|
candleDatas.IndexOf(i) <= endIndex);
|
||||||
|
|
||||||
|
if (datas == null || datas.Count == 0) return;
|
||||||
|
|
||||||
|
candleData.IchimokData.전환선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||||
|
|
||||||
|
datas = candleDatas.FindAll(i =>
|
||||||
|
candleDatas.IndexOf(i) > endIndex - BaselinePeriod &&
|
||||||
|
candleDatas.IndexOf(i) <= endIndex);
|
||||||
|
|
||||||
|
candleData.IchimokData.기준선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||||
|
|
||||||
|
// 후행스팬
|
||||||
|
if (endIndex - Displacement >= 0)
|
||||||
|
{
|
||||||
|
if (candleDatas[candleDatas.Count - Displacement].IchimokData == null)
|
||||||
|
{
|
||||||
|
candleDatas[candleDatas.Count - Displacement].IchimokData = new IchimokData();
|
||||||
|
}
|
||||||
|
candleDatas[candleDatas.Count - Displacement].IchimokData.후행스팬 = endValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Displacement 일전봉 데이터
|
||||||
|
endIndex = candleDatas.Count - 1;
|
||||||
|
endIndex = endIndex - Displacement + 1;
|
||||||
|
|
||||||
|
datas = candleDatas.FindAll(i =>
|
||||||
|
candleDatas.IndexOf(i) > endIndex - ConversionPeriod &&
|
||||||
|
candleDatas.IndexOf(i) <= endIndex);
|
||||||
|
|
||||||
|
if (datas == null || datas.Count == 0) return;
|
||||||
|
|
||||||
|
var displacement_전환선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||||
|
|
||||||
|
datas = candleDatas.FindAll(i =>
|
||||||
|
candleDatas.IndexOf(i) > endIndex - BaselinePeriod &&
|
||||||
|
candleDatas.IndexOf(i) <= endIndex);
|
||||||
|
|
||||||
|
if (datas == null || datas.Count == 0) return;
|
||||||
|
|
||||||
|
var displacement_기준선 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||||
|
|
||||||
|
candleData.IchimokData.선행스팬1 = (displacement_전환선 + displacement_기준선) / 2;
|
||||||
|
|
||||||
|
datas = candleDatas.FindAll(i =>
|
||||||
|
candleDatas.IndexOf(i) > endIndex - LaggingSpan2Period &&
|
||||||
|
candleDatas.IndexOf(i) <= endIndex);
|
||||||
|
|
||||||
|
candleData.IchimokData.선행스팬2 = (datas.Min(i => i.Low) + datas.Max(i => i.High)) / 2;
|
||||||
|
|
||||||
|
candleData.IchimokData.상한선 = Math.Max(candleData.IchimokData.선행스팬1, candleData.IchimokData.선행스팬2);
|
||||||
|
candleData.IchimokData.하한선 = Math.Min(candleData.IchimokData.선행스팬1, candleData.IchimokData.선행스팬2);
|
||||||
|
|
||||||
|
candleData.IchimokData.IsLongUp = candleData.IchimokData.상한선 < candleData.Close;
|
||||||
|
candleData.IchimokData.IsShortDown = candleData.IchimokData.하한선 > candleData.Close;
|
||||||
|
candleData.IchimokData.IsLongDown = candleData.IchimokData.상한선 > candleData.Close;
|
||||||
|
candleData.IchimokData.IsShortUp = candleData.IchimokData.하한선 < candleData.Close;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using Bitget_Server.Domain.Indicators;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Indicators;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ImpulseMacdConditionSetup
|
||||||
|
{
|
||||||
|
private int lengthMA;
|
||||||
|
private int lengthSignal;
|
||||||
|
|
||||||
|
private List<double> srcList = new();
|
||||||
|
private List<double> highList = new();
|
||||||
|
private List<double> lowList = new();
|
||||||
|
private List<double> smmaHighList = new();
|
||||||
|
private List<double> smmaLowList = new();
|
||||||
|
private List<double> zlemaList = new();
|
||||||
|
private List<double> mdList = new();
|
||||||
|
private List<double> sbList = new();
|
||||||
|
private List<double> shList = new();
|
||||||
|
|
||||||
|
public ImpulseMacdConditionSetup(int lengthMA = 34, int lengthSignal = 12)
|
||||||
|
{
|
||||||
|
this.lengthMA = lengthMA;
|
||||||
|
this.lengthSignal = lengthSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImpulseMacdData AddDataPoint(double high, double low, double close, bool flag)
|
||||||
|
{
|
||||||
|
double src = (high + low + close) / 3.0;
|
||||||
|
if (flag)
|
||||||
|
{
|
||||||
|
srcList.Add(src);
|
||||||
|
highList.Add(high);
|
||||||
|
lowList.Add(low);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
srcList[srcList.Count - 1] = src;
|
||||||
|
highList[highList.Count - 1] = high;
|
||||||
|
lowList[lowList.Count - 1] = low;
|
||||||
|
}
|
||||||
|
|
||||||
|
double smmaHigh = CalculateSMMA(highList, smmaHighList, lengthMA);
|
||||||
|
double smmaLow = CalculateSMMA(lowList, smmaLowList, lengthMA);
|
||||||
|
double zlema = CalculateZLEMA(srcList, zlemaList, lengthMA);
|
||||||
|
|
||||||
|
double md = 0.0;
|
||||||
|
if (zlema > smmaHigh) md = zlema - smmaHigh;
|
||||||
|
else if (zlema < smmaLow) md = zlema - smmaLow;
|
||||||
|
|
||||||
|
if (flag) mdList.Add(md);
|
||||||
|
else mdList[mdList.Count - 1] = md;
|
||||||
|
|
||||||
|
double sb = mdList.Skip(mdList.Count - lengthSignal).Average();
|
||||||
|
double sh = md - sb;
|
||||||
|
|
||||||
|
if (flag)
|
||||||
|
{
|
||||||
|
sbList.Add(sb);
|
||||||
|
shList.Add(sh);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sbList[sbList.Count - 1] = sb;
|
||||||
|
shList[shList.Count - 1] = sh;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mdList.Count < lengthSignal) return null;
|
||||||
|
|
||||||
|
if (mdList.Count > 100) mdList.RemoveRange(0, 50);
|
||||||
|
if (sbList.Count > 100) sbList.RemoveRange(0, 50);
|
||||||
|
if (shList.Count > 100) shList.RemoveRange(0, 50);
|
||||||
|
|
||||||
|
return new ImpulseMacdData { MD = md, SH = sh, SB = sb };
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalculateSMMA(List<double> src, List<double> smmaList, int len)
|
||||||
|
{
|
||||||
|
if (src.Count < len) return src.Last();
|
||||||
|
|
||||||
|
double smma;
|
||||||
|
if (smmaList.Count == 0)
|
||||||
|
smma = src.Skip(src.Count - len).Average();
|
||||||
|
else
|
||||||
|
smma = (smmaList.Last() * (len - 1) + src.Last()) / len;
|
||||||
|
|
||||||
|
smmaList.Add(smma);
|
||||||
|
return smma;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalculateZLEMA(List<double> src, List<double> zlemaList, int length)
|
||||||
|
{
|
||||||
|
if (src.Count < length * 2) return src.Last();
|
||||||
|
|
||||||
|
var ema1 = CalculateEMA(src, length);
|
||||||
|
var ema2 = CalculateEMA(ema1, length);
|
||||||
|
double d = ema1.Last() - ema2.Last();
|
||||||
|
double zlema = ema1.Last() + d;
|
||||||
|
zlemaList.Add(zlema);
|
||||||
|
return zlema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<double> CalculateEMA(List<double> src, int length)
|
||||||
|
{
|
||||||
|
List<double> ema = new List<double>();
|
||||||
|
double k = 2.0 / (length + 1);
|
||||||
|
ema.Add(src.Take(length).Average());
|
||||||
|
|
||||||
|
for (int i = length; i < src.Count; i++)
|
||||||
|
{
|
||||||
|
double value = src[i] * k + ema.Last() * (1 - k);
|
||||||
|
ema.Add(value);
|
||||||
|
}
|
||||||
|
return ema;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Indicators;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Indicators;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class MovingAverageConditionSetup
|
||||||
|
{
|
||||||
|
public MovingAverageType MovingAverageType { get; set; }
|
||||||
|
|
||||||
|
// 기간 파라미터 (기존 WinForms와 동일)
|
||||||
|
public int CandleLength24 { get; set; } = 24;
|
||||||
|
public int CandleLength48 { get; set; } = 48;
|
||||||
|
public int CandleLength4 { get; set; } = 4;
|
||||||
|
public int CandleLength5 { get; set; } = 5;
|
||||||
|
public int CandleLength2 { get; set; } = 2;
|
||||||
|
public int CandleLength3 { get; set; } = 3;
|
||||||
|
public int CandleLength36 { get; set; } = 36;
|
||||||
|
public int CandleLength72 { get; set; } = 72;
|
||||||
|
public int CandleLength12 { get; set; } = 12;
|
||||||
|
public int CandleLength8 { get; set; } = 8;
|
||||||
|
public int CandleLength144 { get; set; } = 144;
|
||||||
|
public int CandleLength40 { get; set; } = 40;
|
||||||
|
public int CandleLength20 { get; set; } = 20;
|
||||||
|
public int CandleLength120 { get; set; } = 120;
|
||||||
|
public int CandleLength240 { get; set; } = 240;
|
||||||
|
public int CandleLength480 { get; set; } = 480;
|
||||||
|
public int CandleLength80 { get; set; } = 80;
|
||||||
|
public int CandleLength160 { get; set; } = 160;
|
||||||
|
|
||||||
|
public CandlePriceType MACandlePriceType { get; set; }
|
||||||
|
|
||||||
|
public void Cal_YHK_Downpublic(List<CandleData> candleDatas, CandleData candleData)
|
||||||
|
{
|
||||||
|
var endIndex = candleDatas.Count() - 1;
|
||||||
|
if (candleData == null) return;
|
||||||
|
|
||||||
|
if (candleData.movingAverageDataList == null)
|
||||||
|
{
|
||||||
|
candleData.movingAverageDataList = new Dictionary<int, MovingAverageData>
|
||||||
|
{
|
||||||
|
{ CandleLength2, new MovingAverageData() },
|
||||||
|
{ CandleLength4, new MovingAverageData() },
|
||||||
|
{ CandleLength5, new MovingAverageData() },
|
||||||
|
{ CandleLength12, new MovingAverageData() },
|
||||||
|
{ CandleLength24, new MovingAverageData() },
|
||||||
|
{ CandleLength36, new MovingAverageData() },
|
||||||
|
{ CandleLength144, new MovingAverageData() },
|
||||||
|
{ CandleLength40, new MovingAverageData() },
|
||||||
|
{ CandleLength48, new MovingAverageData() },
|
||||||
|
{ CandleLength72, new MovingAverageData() },
|
||||||
|
{ CandleLength3, new MovingAverageData() },
|
||||||
|
{ CandleLength8, new MovingAverageData() },
|
||||||
|
{ CandleLength20, new MovingAverageData() },
|
||||||
|
{ CandleLength80, new MovingAverageData() },
|
||||||
|
{ CandleLength160, new MovingAverageData() },
|
||||||
|
{ CandleLength120, new MovingAverageData() },
|
||||||
|
{ CandleLength240, new MovingAverageData() },
|
||||||
|
{ CandleLength480, new MovingAverageData() }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in candleData.movingAverageDataList.ToList())
|
||||||
|
{
|
||||||
|
var datas = candleDatas.FindAll(i =>
|
||||||
|
candleDatas.IndexOf(i) > endIndex - item.Key &&
|
||||||
|
candleDatas.IndexOf(i) <= endIndex);
|
||||||
|
|
||||||
|
candleData.movingAverageDataList[item.Key] = new MovingAverageData();
|
||||||
|
candleData.movingAverageDataList[item.Key].Value = datas.Sum(i => i.Close) / item.Key;
|
||||||
|
CalBolangerValue(datas, candleData.movingAverageDataList[item.Key], datas.Count - 1, item.Key);
|
||||||
|
CalBolangerValue(datas, candleData.movingAverageDataList[item.Key], datas.Count - 1, item.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 볼린저 밴드 계산
|
||||||
|
public void CalBolangerValue(List<CandleData> data, MovingAverageData mvData, int i, int period)
|
||||||
|
{
|
||||||
|
double total_average = 0;
|
||||||
|
if (i < period - 1) return;
|
||||||
|
|
||||||
|
for (int t = i - period + 1; t < i; t++)
|
||||||
|
total_average += data[t].Close;
|
||||||
|
total_average += data[i].Close;
|
||||||
|
|
||||||
|
double average = total_average / period;
|
||||||
|
double t_Close = 0;
|
||||||
|
double sum = 0;
|
||||||
|
double powSum = 0;
|
||||||
|
|
||||||
|
int j;
|
||||||
|
for (j = (i - period + 1); j < i; j++)
|
||||||
|
{
|
||||||
|
t_Close += Math.Pow(data[j].Close - average, 2);
|
||||||
|
sum += data[j].Close;
|
||||||
|
powSum += Math.Pow(data[j].Close, 2);
|
||||||
|
}
|
||||||
|
t_Close += Math.Pow(data[j].Close - average, 2);
|
||||||
|
|
||||||
|
mvData.BolangerValue_Down = average - mvData.BolangerSquared * Math.Sqrt(t_Close / period);
|
||||||
|
mvData.BolangerValue_Up = average + mvData.BolangerSquared * Math.Sqrt(t_Close / period);
|
||||||
|
mvData.Average = average;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
namespace Bitget_Server.Application.Indicators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TradingView 스타일 복합 시그널 (Impulse MACD + MACD + SMA + ADX).
|
||||||
|
/// 기존 WinForms 구현을 그대로 이식하였으며 WinForms using 만 제거.
|
||||||
|
/// </summary>
|
||||||
|
public class TradingViewConditionSetup
|
||||||
|
{
|
||||||
|
public int LengthMA = 34;
|
||||||
|
public int LengthSignal = 12;
|
||||||
|
public int AdxLength = 12;
|
||||||
|
public double AdxThreshold = 24;
|
||||||
|
public double RangeThreshold = 1;
|
||||||
|
public double RangeThresholdSell = 1;
|
||||||
|
|
||||||
|
public List<double> High;
|
||||||
|
public List<double> Low;
|
||||||
|
public List<double> Close;
|
||||||
|
|
||||||
|
private double[] Sma4;
|
||||||
|
|
||||||
|
public void SetData(List<double> high, List<double> low, List<double> close)
|
||||||
|
{
|
||||||
|
High = high;
|
||||||
|
Low = low;
|
||||||
|
Close = close;
|
||||||
|
Sma4 = ComputeSma4();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalcSmma(List<double> src, int len)
|
||||||
|
{
|
||||||
|
double smma = src.Take(len).Average();
|
||||||
|
for (int i = len; i < src.Count; i++)
|
||||||
|
smma = (smma * (len - 1) + src[i]) / len;
|
||||||
|
return smma;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalcZlema(List<double> src, int length)
|
||||||
|
{
|
||||||
|
double ema1 = Ema(src, length);
|
||||||
|
double ema2 = Ema(Enumerable.Repeat(ema1, src.Count).ToList(), length);
|
||||||
|
double d = ema1 - ema2;
|
||||||
|
return ema1 + d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double Ema(List<double> src, int length)
|
||||||
|
{
|
||||||
|
double alpha = 2.0 / (length + 1);
|
||||||
|
double ema = src[0];
|
||||||
|
for (int i = 1; i < src.Count; i++)
|
||||||
|
ema = alpha * src[i] + (1 - alpha) * ema;
|
||||||
|
return ema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double[] ComputeSma4()
|
||||||
|
{
|
||||||
|
double[] sma4 = new double[Close.Count];
|
||||||
|
for (int i = 3; i < Close.Count; i++)
|
||||||
|
sma4[i] = (Close[i] + Close[i - 1] + Close[i - 2] + Close[i - 3]) / 4.0;
|
||||||
|
return sma4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double ComputeAdx()
|
||||||
|
{
|
||||||
|
// 원본에서도 더미값 사용. 필요 시 실제 ADX 구현 추가 가능.
|
||||||
|
return 25.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ComputeMd(bool flag = true)
|
||||||
|
{
|
||||||
|
if (flag)
|
||||||
|
{
|
||||||
|
double hi = CalcSmma(High, LengthMA);
|
||||||
|
double lo = CalcSmma(Low, LengthMA);
|
||||||
|
var hlc3 = High.Zip(Low, (h, l) => (h + l + Close.Last()) / 3).ToList();
|
||||||
|
double mi = CalcZlema(hlc3, LengthMA);
|
||||||
|
if (mi > hi) return mi - hi;
|
||||||
|
if (mi < lo) return mi - lo;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var highTemp = new List<double>(High);
|
||||||
|
var lowTemp = new List<double>(Low);
|
||||||
|
var closeTemp = new List<double>(Close);
|
||||||
|
|
||||||
|
highTemp.RemoveAt(highTemp.Count - 1);
|
||||||
|
lowTemp.RemoveAt(lowTemp.Count - 1);
|
||||||
|
closeTemp.RemoveAt(closeTemp.Count - 1);
|
||||||
|
|
||||||
|
double hi = CalcSmma(highTemp, LengthMA);
|
||||||
|
double lo = CalcSmma(lowTemp, LengthMA);
|
||||||
|
var hlc3 = highTemp.Zip(lowTemp, (h, l) => (h + l + closeTemp.Last()) / 3).ToList();
|
||||||
|
double mi = CalcZlema(hlc3, LengthMA);
|
||||||
|
if (mi > hi) return mi - hi;
|
||||||
|
if (mi < lo) return mi - lo;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (double macdLine, double signalLine) ComputeMacd()
|
||||||
|
{
|
||||||
|
double macdLine = Ema(Close, 34) - Ema(Close, 12);
|
||||||
|
double signalLine = Ema(Enumerable.Repeat(macdLine, Close.Count).ToList(), 12);
|
||||||
|
return (macdLine, signalLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
public (double macdLine, double signalLine) PrevComputeMacd()
|
||||||
|
{
|
||||||
|
var temp = new List<double>(Close);
|
||||||
|
temp.RemoveAt(temp.Count - 1);
|
||||||
|
double macdLine = Ema(temp, 34) - Ema(temp, 12);
|
||||||
|
double signalLine = Ema(Enumerable.Repeat(macdLine, temp.Count).ToList(), 12);
|
||||||
|
return (macdLine, signalLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRangeBound()
|
||||||
|
{
|
||||||
|
double highestHigh = High.Skip(High.Count - 12).Max();
|
||||||
|
double lowestLow = Low.Skip(Low.Count - 12).Min();
|
||||||
|
double range = highestHigh - lowestLow;
|
||||||
|
return range < (highestHigh * RangeThreshold / 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRangeBoundSell()
|
||||||
|
{
|
||||||
|
double highestHigh = High.Skip(High.Count - 12).Max();
|
||||||
|
double lowestLow = Low.Skip(Low.Count - 12).Min();
|
||||||
|
double range = highestHigh - lowestLow;
|
||||||
|
return range < (highestHigh * RangeThresholdSell / 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTrending() => ComputeAdx() > AdxThreshold;
|
||||||
|
|
||||||
|
public double GetAvgData(int range)
|
||||||
|
{
|
||||||
|
var sma = 0.0;
|
||||||
|
for (var i = 0; i < range; i++)
|
||||||
|
sma += Close[Close.Count - (i + 1)];
|
||||||
|
return sma / range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double GetAvgData(int range, int prev)
|
||||||
|
{
|
||||||
|
var sma = 0.0;
|
||||||
|
for (var i = 0; i < range; i++)
|
||||||
|
sma += Close[Close.Count - (i + 1 + prev)];
|
||||||
|
return sma / range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLongSignal()
|
||||||
|
{
|
||||||
|
double md = ComputeMd();
|
||||||
|
double prevMd = ComputeMd(false);
|
||||||
|
var (macd, signal) = ComputeMacd();
|
||||||
|
var (prevMacd, prevSignal) = PrevComputeMacd();
|
||||||
|
|
||||||
|
return ((prevMd == 0 && md > 0) || (prevMacd < prevSignal && macd > prevMacd && macd > signal && md > 0)) &&
|
||||||
|
IsTrending() && IsSmaMiddle(4, 12, true, GetAvgData(72)) && GetAvgData(4) > GetAvgData(4, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsShortSignal()
|
||||||
|
{
|
||||||
|
double md = ComputeMd();
|
||||||
|
double prevMd = ComputeMd(false);
|
||||||
|
var (macd, signal) = ComputeMacd();
|
||||||
|
var (prevMacd, prevSignal) = PrevComputeMacd();
|
||||||
|
|
||||||
|
return ((prevMd == 0 && md < 0) || (prevMacd > prevSignal && macd < prevMacd && macd < signal && md < 0)) &&
|
||||||
|
IsTrending() && IsSmaMiddle(4, 12, false, GetAvgData(72)) && GetAvgData(4) < GetAvgData(4, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSmaHigh(int range, int count)
|
||||||
|
{
|
||||||
|
var list = new List<double>();
|
||||||
|
for (int i = Close.Count - count - 1; i < count; i++)
|
||||||
|
{
|
||||||
|
var sum = 0.0;
|
||||||
|
for (int j = 0; j < range; j++) sum += Close[i - j];
|
||||||
|
list.Add(sum / range);
|
||||||
|
}
|
||||||
|
return list.Max() == list[list.Count - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSmaLow(int range, int count)
|
||||||
|
{
|
||||||
|
var list = new List<double>();
|
||||||
|
for (int i = Close.Count - count - 1; i < count; i++)
|
||||||
|
{
|
||||||
|
var sum = 0.0;
|
||||||
|
for (int j = 0; j < range; j++) sum += Close[i - j];
|
||||||
|
list.Add(sum / range);
|
||||||
|
}
|
||||||
|
return list.Min() == list[list.Count - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSmaMiddle(int range, int count, bool longFlag, double value)
|
||||||
|
{
|
||||||
|
var list = new List<double>();
|
||||||
|
for (int i = Close.Count - count - 1; i < count; i++)
|
||||||
|
{
|
||||||
|
var sum = 0.0;
|
||||||
|
for (int j = 0; j < range; j++) sum += Close[i - j];
|
||||||
|
list.Add(sum / range);
|
||||||
|
}
|
||||||
|
if (longFlag) return value < list.Average();
|
||||||
|
return value > list.Average();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Bitget_Server/Application/Options/BitgetOptions.cs
Normal file
38
Bitget_Server/Application/Options/BitgetOptions.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// appsettings.json의 Bitget 섹션 바인딩용.
|
||||||
|
/// </summary>
|
||||||
|
public class BitgetOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "Bitget";
|
||||||
|
|
||||||
|
/// <summary>기본 API 키 ID (ApiKeys 목록 중 하나).</summary>
|
||||||
|
public int DefaultApiId { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>초당 허용 API 호출 횟수 (Bitget Rate Limit).</summary>
|
||||||
|
public int RateLimitPerSecond { get; set; } = 20;
|
||||||
|
|
||||||
|
/// <summary>JSON 설정 파일 저장 경로. 컨테이너에서는 /data 로 볼륨 마운트.</summary>
|
||||||
|
public string DataDirectory { get; set; } = "./data";
|
||||||
|
|
||||||
|
/// <summary>로그 디렉토리. 컨테이너에서는 /logs 로 볼륨 마운트.</summary>
|
||||||
|
public string LogDirectory { get; set; } = "./logs";
|
||||||
|
|
||||||
|
/// <summary>Debug 레벨 로그 활성화 여부.</summary>
|
||||||
|
public bool LogDebugWrite { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>자동 업데이트 주기(ms). BackgroundService에서 사용.</summary>
|
||||||
|
public int MarketInfoUpdateIntervalMs { get; set; } = 300;
|
||||||
|
|
||||||
|
/// <summary>WebSocket 처리 주기(ms).</summary>
|
||||||
|
public int WebSocketCalcIntervalMs { get; set; } = 1000;
|
||||||
|
|
||||||
|
/// <summary>Ping 주기(ms). Bitget는 30초 이내 응답 필요.</summary>
|
||||||
|
public int WebSocketPingIntervalMs { get; set; } = 25000;
|
||||||
|
|
||||||
|
/// <summary>사용자별 Bitget API 키 목록 (기존 하드코딩 3개에 해당).</summary>
|
||||||
|
public List<ApiKeyInfo_Bitget> ApiKeys { get; set; } = new();
|
||||||
|
}
|
||||||
28
Bitget_Server/Application/Options/GlobalOptions.cs
Normal file
28
Bitget_Server/Application/Options/GlobalOptions.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Bitget_Server.Infrastructure.Utils;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 런타임 변경 가능한 전역 옵션. JSON 파일에 영속화됨.
|
||||||
|
/// (레버리지/주문수량/선택된 API 등 사용자가 런타임에 변경하는 값)
|
||||||
|
/// </summary>
|
||||||
|
public class GlobalOptions : BaseSettings<GlobalOptions>
|
||||||
|
{
|
||||||
|
public bool LogDebugWrite { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 선택된 API 키 정보 (appsettings.json의 ApiKeys 중 하나의 스냅샷).
|
||||||
|
/// </summary>
|
||||||
|
public ApiKeyInfo_Bitget SelectAPI { get; set; } = null;
|
||||||
|
|
||||||
|
public double Leverage { get; set; } = 10;
|
||||||
|
|
||||||
|
public double OrderAmount { get; set; } = 50;
|
||||||
|
|
||||||
|
/// <summary>기본 캔들 간격(분).</summary>
|
||||||
|
public int CandleIntervalMinutes { get; set; } = 5;
|
||||||
|
|
||||||
|
/// <summary>선택된 코인 종목 (Bitget symbol, 예: BTCUSDT).</summary>
|
||||||
|
public string SelectedCoin { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
285
Bitget_Server/Application/Services/BitgetTradingService.cs
Normal file
285
Bitget_Server/Application/Services/BitgetTradingService.cs
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using Bitget_Server.Application.Options;
|
||||||
|
using Bitget_Server.Domain;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Infrastructure.Bitget;
|
||||||
|
using Bitget_Server.Infrastructure.Utils;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 IndicatorProcess의 Private API 영역(주문/취소/잔고/포지션모드 변경/레버리지 변경/체결상세)을 이식한 서비스.
|
||||||
|
/// HMAC-SHA256 서명은 BitgetClient.GenerateSignString을 사용한다.
|
||||||
|
/// 외부 호출 경로는 BitgetClient.SendWebServerAsync → BitgetRateLimiter(20/sec) 순으로 흐른다.
|
||||||
|
/// </summary>
|
||||||
|
public class BitgetTradingService
|
||||||
|
{
|
||||||
|
private readonly BitgetClient _client;
|
||||||
|
private readonly GlobalOptions _options;
|
||||||
|
private readonly ILogger<BitgetTradingService> _logger;
|
||||||
|
|
||||||
|
public BitgetTradingService(
|
||||||
|
BitgetClient client,
|
||||||
|
GlobalOptions options,
|
||||||
|
ILogger<BitgetTradingService> logger)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_options = options;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string AccessKey => _options.SelectAPI?.AccessKey ?? string.Empty;
|
||||||
|
private string SecretKey => _options.SelectAPI?.SecretKey ?? string.Empty;
|
||||||
|
private string Passphrase => _options.SelectAPI?.PassphraseKey ?? string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Bitget 서버 타임스탬프 조회.</summary>
|
||||||
|
public async Task<long> GetTimeStampAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetGetTimeStamp,
|
||||||
|
BitgetEndpoints.GetTimestamp,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
|
||||||
|
if (result == null) return 0;
|
||||||
|
|
||||||
|
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||||||
|
JsonConvert.SerializeObject(result["data"]));
|
||||||
|
|
||||||
|
long.TryParse(data["serverTime"].ToString(), out var ts);
|
||||||
|
return ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>미체결 주문 조회.</summary>
|
||||||
|
public async Task<Dictionary<string, object>> GetPendingOrdersAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
const string reqPath = "/api/v2/mix/order/orders-pending";
|
||||||
|
|
||||||
|
var nvc = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, "productType=USDT-FUTURES", null, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetPendingOrder_Future,
|
||||||
|
BitgetEndpoints.PendingOrders_Future,
|
||||||
|
headers: nvc,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>선물 계좌 조회.</summary>
|
||||||
|
public async Task<List<Dictionary<string, object>>> GetFutureAccountListAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
const string reqPath = "/api/v2/mix/account/accounts";
|
||||||
|
|
||||||
|
var nvc = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, "productType=USDT-FUTURES", null, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetAccountList_Future,
|
||||||
|
BitgetEndpoints.AccountList_Future,
|
||||||
|
headers: nvc,
|
||||||
|
ct: ct).ConfigureAwait(false) as List<Dictionary<string, object>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>주문 취소.</summary>
|
||||||
|
public async Task<Dictionary<string, object>> CancelFutureOrderAsync(string symbol, string orderId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
const string reqPath = "/api/v2/mix/order/cancel-order";
|
||||||
|
var postStr = "{\"symbol\" : \"" + symbol + "\", \"orderId\" : \"" + orderId + "\", \"productType\" : \"usdt-futures\" , \"marginCoin\" : \"USDT\"}";
|
||||||
|
|
||||||
|
var nvc = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetCancelOrder_Future,
|
||||||
|
BitgetEndpoints.CancelOrder_Future,
|
||||||
|
postData: postStr,
|
||||||
|
headers: nvc,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>포지션 모드 변경 (one_way / hedge).</summary>
|
||||||
|
public async Task<Dictionary<string, object>> ChangeFuturePositionModeAsync(FuturePositionMode mode, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
const string reqPath = "/api/v2/mix/account/set-position-mode";
|
||||||
|
var postStr = "{\"productType\" : \"USDT-FUTURES\", \"posMode\" : \"" + Convertor.GetEnumDescription(mode) + "\"}";
|
||||||
|
|
||||||
|
var nvc = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetChangePositionMode_Future,
|
||||||
|
BitgetEndpoints.ChangePositionMode_Future,
|
||||||
|
postData: postStr,
|
||||||
|
headers: nvc,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>주문 상세 조회.</summary>
|
||||||
|
public async Task<Dictionary<string, object>> GetFutureOrderDetailAsync(string symbol, string orderId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
const string reqPath = "/api/v2/mix/order/detail";
|
||||||
|
var queryString = $"symbol={symbol}&orderId={orderId}&productType=USDT-FUTURES";
|
||||||
|
|
||||||
|
var nvc = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "GET", reqPath, queryString, null, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
var url = BitgetEndpoints.OrderDetail_Future + "?" + queryString;
|
||||||
|
return await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetOrderDetail_Future,
|
||||||
|
url,
|
||||||
|
headers: nvc,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 선물 주문 (Buy는 선행적으로 MarginMode/Leverage 변경, Sell은 사전 Cancel).
|
||||||
|
/// 기존 IndicatorProcess.FutureOrder 로직 그대로 이식.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Dictionary<string, object>> FutureOrderAsync(
|
||||||
|
string symbol,
|
||||||
|
FutureMarginMode marginMode,
|
||||||
|
double size,
|
||||||
|
double price,
|
||||||
|
FutureSide futureSide,
|
||||||
|
OrderType orderType,
|
||||||
|
double leverage,
|
||||||
|
string id = "",
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var side = string.Empty;
|
||||||
|
var tradeSide = string.Empty;
|
||||||
|
|
||||||
|
switch (futureSide)
|
||||||
|
{
|
||||||
|
case FutureSide.BuyShort:
|
||||||
|
id = Guid.NewGuid().ToString();
|
||||||
|
side = "sell"; tradeSide = "open"; break;
|
||||||
|
case FutureSide.BuyLong:
|
||||||
|
id = Guid.NewGuid().ToString();
|
||||||
|
side = "buy"; tradeSide = "Open"; break;
|
||||||
|
case FutureSide.SellShort:
|
||||||
|
side = "sell"; tradeSide = "close"; break;
|
||||||
|
case FutureSide.SellLong:
|
||||||
|
side = "buy"; tradeSide = "close"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderType == OrderType.market)
|
||||||
|
price = 0;
|
||||||
|
|
||||||
|
var timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
|
||||||
|
// Buy 주문: Margin Mode → Leverage → Order 체인
|
||||||
|
if (futureSide == FutureSide.BuyShort || futureSide == FutureSide.BuyLong)
|
||||||
|
{
|
||||||
|
var changeMarginModeStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"usdt-futures\", \"marginCoin\": \"USDT\", \"marginMode\": \""
|
||||||
|
+ Convertor.GetEnumDescription(marginMode) + "\"}";
|
||||||
|
|
||||||
|
var nvcMargin = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", "/api/v2/mix/account/set-margin-mode", "", changeMarginModeStr, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
var marginResult = await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetChangeMarginMode_Future,
|
||||||
|
BitgetEndpoints.ChangeMarginMode_Future,
|
||||||
|
postData: changeMarginModeStr,
|
||||||
|
headers: nvcMargin,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
|
||||||
|
if (marginResult == null || marginResult["msg"]?.ToString() != "success")
|
||||||
|
{
|
||||||
|
_logger.LogWarning("MarginMode 변경 실패: {Symbol}", symbol);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var leverageStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"USDT-FUTURES\", \"marginCoin\": \"USDT\", \"leverage\": \""
|
||||||
|
+ leverage.ToString() + "\", \"holdSide\": \"" + (futureSide == FutureSide.BuyShort ? "short" : "long") + "\"}";
|
||||||
|
|
||||||
|
timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
var nvcLeverage = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", "/api/v2/mix/account/set-leverage", "", leverageStr, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
var leverageResult = await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetChangeLeverage_Future,
|
||||||
|
BitgetEndpoints.ChangeLeverage_Future,
|
||||||
|
postData: leverageStr,
|
||||||
|
headers: nvcLeverage,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
|
||||||
|
if (leverageResult == null || leverageResult["msg"]?.ToString() != "success")
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Leverage 변경 실패: {Symbol}", symbol);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Sell 주문: 미체결 주문 취소 후 진행
|
||||||
|
var cancelResult = await CancelFutureOrderAsync(symbol, id, ct).ConfigureAwait(false);
|
||||||
|
if (cancelResult == null || cancelResult["msg"]?.ToString() != "success")
|
||||||
|
_logger.LogError("Sell Cancel 실패: {Symbol} / {OrderId}", symbol, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실제 주문 전송
|
||||||
|
const string reqPath = "/api/v2/mix/order/place-order";
|
||||||
|
var postStr = "{\"symbol\": \"" + symbol + "\", \"productType\": \"usdt-futures\", \"marginMode\": \""
|
||||||
|
+ Convertor.GetEnumDescription(marginMode) + "\", \"marginCoin\": \"USDT\", \"size\": \"" + size.ToString()
|
||||||
|
+ "\", \"price\": \"" + price.ToString() + "\", \"side\": \"" + side + "\", \"tradeSide\": \"" + tradeSide
|
||||||
|
+ "\", \"orderType\": \"" + Convertor.GetEnumDescription(orderType)
|
||||||
|
+ "\", \"force\": \"gtc\", \"clientOid\": \"" + id + "\" }";
|
||||||
|
|
||||||
|
timestamp = (await GetTimeStampAsync(ct).ConfigureAwait(false)).ToString();
|
||||||
|
var nvc = new NameValueCollection
|
||||||
|
{
|
||||||
|
{ "ACCESS-KEY", AccessKey },
|
||||||
|
{ "ACCESS-SIGN", BitgetClient.GenerateSignString(timestamp, "POST", reqPath, "", postStr, SecretKey) },
|
||||||
|
{ "ACCESS-PASSPHRASE", Passphrase },
|
||||||
|
{ "ACCESS-TIMESTAMP", timestamp }
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetOrder_Future,
|
||||||
|
BitgetEndpoints.BuySellOrder_Future,
|
||||||
|
postData: postStr,
|
||||||
|
headers: nvc,
|
||||||
|
ct: ct).ConfigureAwait(false) as Dictionary<string, object>;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
Bitget_Server/Application/Services/InitialCandleLoader.cs
Normal file
78
Bitget_Server/Application/Services/InitialCandleLoader.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Bitget_Server.Infrastructure.Bitget;
|
||||||
|
using Bitget_Server.Infrastructure.Utils;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 MainForm.getMarketCandleData_WebService 이식.
|
||||||
|
/// 신호 검색 대상 마켓/인터벌 조합의 초기 캔들 데이터를 일괄 로드한다.
|
||||||
|
/// BitgetRateLimiter 를 자연스럽게 경유하므로 별도 Thread.Sleep(50) 이 불필요.
|
||||||
|
/// </summary>
|
||||||
|
public class InitialCandleLoader
|
||||||
|
{
|
||||||
|
private const int CandleCount = 200;
|
||||||
|
|
||||||
|
private readonly BitgetClient _client;
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly ILogger<InitialCandleLoader> _logger;
|
||||||
|
|
||||||
|
public InitialCandleLoader(BitgetClient client, MarketState state, ILogger<InitialCandleLoader> logger)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_state = state;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 체크된 마켓 목록에 대해 캔들 데이터를 채운다.
|
||||||
|
/// </summary>
|
||||||
|
public async Task LoadAsync(IEnumerable<string> marketNames, IEnumerable<string> intervalDescriptions, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var intervalSet = new HashSet<string>(intervalDescriptions ?? Array.Empty<string>());
|
||||||
|
|
||||||
|
foreach (var marketName in marketNames ?? Array.Empty<string>())
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
MarketCandleDataList marketCandleDataList;
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
marketCandleDataList = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == marketName);
|
||||||
|
}
|
||||||
|
if (marketCandleDataList == null) continue;
|
||||||
|
|
||||||
|
foreach (MCandleTimeType interval in Enum.GetValues(typeof(MCandleTimeType)))
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
var description = Convertor.GetEnumDescription(interval);
|
||||||
|
if (!intervalSet.Contains(description)) continue;
|
||||||
|
|
||||||
|
var url = string.Format(
|
||||||
|
BitgetEndpoints.MarketCandle_Future,
|
||||||
|
marketName,
|
||||||
|
description,
|
||||||
|
CandleCount.ToString(),
|
||||||
|
"USDT-FUTURES");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetCandleStick_Future,
|
||||||
|
url,
|
||||||
|
marketCandleDataList,
|
||||||
|
interval,
|
||||||
|
ct: ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "초기 캔들 로드 실패 ({Market} / {Interval})", marketName, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
Bitget_Server/Application/Services/PriceLevelTracker.cs
Normal file
146
Bitget_Server/Application/Services/PriceLevelTracker.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 IndicatorProcess.Calculate전고점 / 전저고점 dictionary bookkeeping 이식.
|
||||||
|
/// 단일 인스턴스로 보관되며 키는 "MarketName + Interval(Description)" 문자열이다.
|
||||||
|
/// </summary>
|
||||||
|
public class PriceLevelTracker
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Dictionary<string, double>> _levels = new();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, Dictionary<string, double>> Levels => _levels;
|
||||||
|
|
||||||
|
public void Clear() => _levels.Clear();
|
||||||
|
|
||||||
|
public bool ContainsKey(string key) => _levels.ContainsKey(key);
|
||||||
|
|
||||||
|
public Dictionary<string, double> GetOrCreate(string key)
|
||||||
|
{
|
||||||
|
if (_levels.TryGetValue(key, out var existing)) return existing;
|
||||||
|
|
||||||
|
var dic = new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
{ "전고가", 0 }, { "전저가", 0 },
|
||||||
|
{ "전전고가", 0 }, { "전전저가", 0 },
|
||||||
|
{ "전전전저가", 0 }, { "전전전고가", 0 },
|
||||||
|
{ "전전전전저가", 0 }, { "전전전전고가", 0 },
|
||||||
|
{ "tempHighVal", 0 }, { "tempLowVal", 0 },
|
||||||
|
{ "tempHigh2Val", 0 }, { "tempLow2Val", 0 },
|
||||||
|
{ "tempHigh3Val", 0 }, { "tempLow3Val", 0 },
|
||||||
|
{ "tempHigh4Val", 0 }, { "tempLow4Val", 0 },
|
||||||
|
{ "switchLowVal", 0 }, { "switchHighVal", 0 },
|
||||||
|
{ "Type", 0 }
|
||||||
|
};
|
||||||
|
_levels[key] = dic;
|
||||||
|
return dic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 IndicatorProcess.Calculate전고점 이식.
|
||||||
|
/// 전봉/전전봉/전전전봉의 이동평균(4일) 값으로 local minima/maxima 를 추적한다.
|
||||||
|
/// </summary>
|
||||||
|
public bool Calculate(string market, string intervalKey, MCandleTimeType type,
|
||||||
|
double 전봉, double 전전봉, double 전전전봉, CandleData candleData)
|
||||||
|
{
|
||||||
|
var key = market + intervalKey;
|
||||||
|
var 저고점 = GetOrCreate(key);
|
||||||
|
|
||||||
|
var nowUnix = ((DateTimeOffset)candleData.Argument).ToUnixTimeSeconds();
|
||||||
|
|
||||||
|
// === 전저가 계산 ===
|
||||||
|
if (전봉 > 전전봉 && 전전봉 < 전전전봉 &&
|
||||||
|
저고점["tempHighVal"] >= 저고점["tempLowVal"] &&
|
||||||
|
저고점["tempLowVal"] != nowUnix &&
|
||||||
|
nowUnix - 저고점["tempHighVal"] >= 120 * (int)type)
|
||||||
|
{
|
||||||
|
저고점["tempLow4Val"] = 저고점["tempLowVal"];
|
||||||
|
저고점["tempLowVal"] = 저고점["tempLow2Val"];
|
||||||
|
저고점["tempLow2Val"] = 저고점["tempLowVal"];
|
||||||
|
저고점["tempLowVal"] = nowUnix;
|
||||||
|
저고점["switchLowVal"] = 저고점["전전전전저가"];
|
||||||
|
저고점["전전전전저가"] = 저고점["전전저가"];
|
||||||
|
저고점["전전전저가"] = 저고점["전전저가"];
|
||||||
|
저고점["전전저가"] = 저고점["전저가"];
|
||||||
|
저고점["전저가"] = 전전봉;
|
||||||
|
저고점["Type"] = 0;
|
||||||
|
}
|
||||||
|
else if (전봉 > 전전봉 && 전전봉 < 전전전봉 &&
|
||||||
|
저고점["tempHighVal"] < 저고점["tempLowVal"] &&
|
||||||
|
저고점["tempLowVal"] != nowUnix &&
|
||||||
|
nowUnix - 저고점["tempHighVal"] >= 120 * (int)type)
|
||||||
|
{
|
||||||
|
if (저고점["전저가"] > 전전봉)
|
||||||
|
{
|
||||||
|
저고점["tempLowVal"] = nowUnix;
|
||||||
|
저고점["전저가"] = 전전봉;
|
||||||
|
}
|
||||||
|
저고점["Type"] = 0;
|
||||||
|
}
|
||||||
|
else if (전봉 > 전전봉 && 전전봉 < 전전전봉 &&
|
||||||
|
저고점["tempLowVal"] != nowUnix &&
|
||||||
|
nowUnix - 저고점["tempHighVal"] < 120 * (int)type)
|
||||||
|
{
|
||||||
|
// 이전 상태로 롤백 (High)
|
||||||
|
저고점["전고가"] = 저고점["전전고가"];
|
||||||
|
저고점["전전고가"] = 저고점["전전전고가"];
|
||||||
|
저고점["전전전고가"] = 저고점["전전전전고가"];
|
||||||
|
저고점["Type"] = 0;
|
||||||
|
저고점["전전전전고가"] = 저고점["switchLowVal"];
|
||||||
|
저고점["tempHighVal"] = 저고점["tempHigh2Val"];
|
||||||
|
저고점["tempHigh2Val"] = 저고점["tempHighVal"];
|
||||||
|
저고점["tempHighVal"] = 저고점["tempHigh4Val"];
|
||||||
|
|
||||||
|
if (저고점["전저가"] > 전전봉) 저고점["전저가"] = 전전봉;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 전고가 계산 ===
|
||||||
|
if (전전봉 > 전봉 && 전전봉 > 전전전봉 &&
|
||||||
|
저고점["tempHighVal"] <= 저고점["tempLowVal"] &&
|
||||||
|
저고점["tempHighVal"] != nowUnix &&
|
||||||
|
nowUnix - 저고점["tempLowVal"] >= 120 * (int)type)
|
||||||
|
{
|
||||||
|
저고점["tempHigh4Val"] = 저고점["tempHighVal"];
|
||||||
|
저고점["tempHighVal"] = 저고점["tempHigh2Val"];
|
||||||
|
저고점["tempHigh2Val"] = 저고점["tempHighVal"];
|
||||||
|
저고점["tempHighVal"] = nowUnix;
|
||||||
|
저고점["switchHighVal"] = 저고점["전전전전고가"];
|
||||||
|
저고점["전전전전고가"] = 저고점["전전전고가"];
|
||||||
|
저고점["전전전고가"] = 저고점["전전고가"];
|
||||||
|
저고점["전전고가"] = 저고점["전고가"];
|
||||||
|
저고점["Type"] = 1;
|
||||||
|
저고점["전고가"] = 전전봉;
|
||||||
|
}
|
||||||
|
else if (전전봉 > 전봉 && 전전봉 > 전전전봉 &&
|
||||||
|
저고점["tempHighVal"] > 저고점["tempLowVal"] &&
|
||||||
|
저고점["tempHighVal"] != nowUnix &&
|
||||||
|
nowUnix - 저고점["tempLowVal"] >= 120 * (int)type)
|
||||||
|
{
|
||||||
|
if (저고점["전고가"] < 전전봉)
|
||||||
|
{
|
||||||
|
저고점["tempHighVal"] = nowUnix;
|
||||||
|
저고점["전고가"] = 전전봉;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (전전봉 > 전봉 && 전전봉 > 전전전봉 &&
|
||||||
|
저고점["tempHighVal"] != nowUnix &&
|
||||||
|
nowUnix - 저고점["tempLowVal"] < 120 * (int)type)
|
||||||
|
{
|
||||||
|
// 이전 상태로 롤백 (Low)
|
||||||
|
저고점["전저가"] = 저고점["전전저가"];
|
||||||
|
저고점["전전저가"] = 저고점["전전전저가"];
|
||||||
|
저고점["전전전저가"] = 저고점["전전전전저가"];
|
||||||
|
저고점["전전전전저가"] = 저고점["switchHighVal"];
|
||||||
|
저고점["Type"] = 1;
|
||||||
|
저고점["tempLowVal"] = 저고점["tempLow2Val"];
|
||||||
|
저고점["tempLow2Val"] = 저고점["tempLowVal"];
|
||||||
|
저고점["tempLowVal"] = 저고점["tempLow4Val"];
|
||||||
|
|
||||||
|
if (저고점["전고가"] < 전전봉) 저고점["전고가"] = 전전봉;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 저고점["전전전저가"] != 0 && 저고점["전전전고가"] != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Bitget_Server/Application/Services/SettingsService.cs
Normal file
100
Bitget_Server/Application/Services/SettingsService.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using Bitget_Server.Application.Options;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Bitget_Server.Infrastructure.Bitget;
|
||||||
|
using Bitget_Server.Infrastructure.Utils;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms MainForm 의 ComboBox/Setting/Selected 상태를 REST 에서 다루기 위한 서비스.
|
||||||
|
/// </summary>
|
||||||
|
public class SettingsService
|
||||||
|
{
|
||||||
|
private readonly GlobalOptions _global;
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly BitgetClient _client;
|
||||||
|
private readonly BitgetOptions _bitgetOptions;
|
||||||
|
private readonly ILogger<SettingsService> _logger;
|
||||||
|
|
||||||
|
public SettingsService(
|
||||||
|
GlobalOptions global,
|
||||||
|
MarketState state,
|
||||||
|
BitgetClient client,
|
||||||
|
IOptions<BitgetOptions> bitgetOptions,
|
||||||
|
ILogger<SettingsService> logger)
|
||||||
|
{
|
||||||
|
_global = global;
|
||||||
|
_state = state;
|
||||||
|
_client = client;
|
||||||
|
_bitgetOptions = bitgetOptions.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 캔들 간격 설정 =====
|
||||||
|
public int GetCandleIntervalMinutes() => _global.CandleIntervalMinutes;
|
||||||
|
|
||||||
|
public void SetCandleIntervalMinutes(int minutes)
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(typeof(MCandleTimeType), minutes))
|
||||||
|
throw new ArgumentException($"유효하지 않은 캔들 간격: {minutes}");
|
||||||
|
|
||||||
|
_global.CandleIntervalMinutes = minutes;
|
||||||
|
_global.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<object> ListCandleIntervals() =>
|
||||||
|
Enum.GetValues(typeof(MCandleTimeType))
|
||||||
|
.Cast<MCandleTimeType>()
|
||||||
|
.Select(t => new
|
||||||
|
{
|
||||||
|
Name = t.ToString(),
|
||||||
|
Description = Convertor.GetEnumDescription(t),
|
||||||
|
Minutes = (int)t
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 선택 코인 =====
|
||||||
|
public string GetSelectedCoin() => _global.SelectedCoin;
|
||||||
|
|
||||||
|
public void SetSelectedCoin(string marketName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(marketName))
|
||||||
|
throw new ArgumentException("코인 심볼이 비어있습니다.");
|
||||||
|
|
||||||
|
_global.SelectedCoin = marketName;
|
||||||
|
_global.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 코인 목록 조회 =====
|
||||||
|
public async Task<IEnumerable<MarketInfo>> GetCoinsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// BitgetClient.Parse 가 _state.marketInfoList 에 기록한 값을 반환
|
||||||
|
var result = await _client.SendWebServerAsync(
|
||||||
|
ExchangeWebReqType.BitgetMarket_Future,
|
||||||
|
ct: ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (result is List<MarketInfo> list) return list;
|
||||||
|
|
||||||
|
lock (_state.SyncRoot) return _state.marketInfoList.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 레버리지 / 주문수량 / Selected API =====
|
||||||
|
public double GetLeverage() => _global.Leverage;
|
||||||
|
public double GetOrderAmount() => _global.OrderAmount;
|
||||||
|
|
||||||
|
public void SetLeverage(double v) { _global.Leverage = v; _global.Save(); }
|
||||||
|
public void SetOrderAmount(double v) { _global.OrderAmount = v; _global.Save(); }
|
||||||
|
|
||||||
|
public int? GetSelectedApiId() => _global.SelectAPI?.Id;
|
||||||
|
|
||||||
|
public void SetSelectedApiId(int id)
|
||||||
|
{
|
||||||
|
var api = _bitgetOptions.ApiKeys.FirstOrDefault(k => k.Id == id)
|
||||||
|
?? throw new KeyNotFoundException($"Bitget:ApiKeys 에 Id={id} 없음");
|
||||||
|
|
||||||
|
_global.SelectAPI = api;
|
||||||
|
_global.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
386
Bitget_Server/Application/Services/SignalProcessService.cs
Normal file
386
Bitget_Server/Application/Services/SignalProcessService.cs
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
using Bitget_Server.Application.Indicators;
|
||||||
|
using Bitget_Server.Application.Options;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Indicators;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Bitget_Server.Infrastructure.Utils;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 IndicatorProcess.calProcess 이식.
|
||||||
|
/// WinForms Thread 무한 루프는 제거되고, 본 서비스의 ProcessOnceAsync 를
|
||||||
|
/// SignalInfoBackgroundService 가 주기적으로 호출한다.
|
||||||
|
/// </summary>
|
||||||
|
public class SignalProcessService
|
||||||
|
{
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly GlobalOptions _options;
|
||||||
|
private readonly BitgetTradingService _trading;
|
||||||
|
private readonly PriceLevelTracker _levels;
|
||||||
|
private readonly ILogger<SignalProcessService> _logger;
|
||||||
|
|
||||||
|
// signals_LU / signals_SD / signals_LD / signals_SU (기존 public 필드 이식)
|
||||||
|
public List<SignalInfoData> SignalsLongUp { get; } = new();
|
||||||
|
public List<SignalInfoData> SignalsShortDown { get; } = new();
|
||||||
|
public List<SignalInfoData> SignalsLongDown { get; } = new();
|
||||||
|
public List<SignalInfoData> SignalsShortUp { get; } = new();
|
||||||
|
|
||||||
|
public TimeSpan SessionTime { get; set; } = TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
|
// 조건식 상태 (기존 bool 플래그)
|
||||||
|
public bool Checked구름떼상향돌파 { get; set; } = false;
|
||||||
|
public bool Checked구름떼하향돌파 { get; set; } = false;
|
||||||
|
public bool 영호조건식_상향돌파 { get; set; } = false;
|
||||||
|
public bool 영호조건식_하향돌파 { get; set; } = false;
|
||||||
|
|
||||||
|
// 거래 리스트 (기존 IndicatorProcess의 Up/Down/Buy/Sell 딕셔너리)
|
||||||
|
public Dictionary<string, Dictionary<string, object>> UpBuyList { get; } = new();
|
||||||
|
public List<Dictionary<string, object>> UpResultList { get; } = new();
|
||||||
|
public Dictionary<string, Dictionary<string, object>> DownBuyList { get; } = new();
|
||||||
|
public List<Dictionary<string, object>> DownResultList { get; } = new();
|
||||||
|
|
||||||
|
public List<Dictionary<string, object>> BuyLongList { get; } = new();
|
||||||
|
public List<Dictionary<string, object>> BuyShortList { get; } = new();
|
||||||
|
public List<Dictionary<string, object>> ResultList { get; } = new();
|
||||||
|
|
||||||
|
public Dictionary<string, int> BuyShortStep { get; } = new();
|
||||||
|
public Dictionary<string, int> BuyLongStep { get; } = new();
|
||||||
|
|
||||||
|
public int LongCount { get; private set; } = 0;
|
||||||
|
public int ShortCount { get; private set; } = 0;
|
||||||
|
|
||||||
|
private bool _전저고점Flag = true;
|
||||||
|
private bool _positionModeInitialized = false;
|
||||||
|
|
||||||
|
public SignalProcessService(
|
||||||
|
MarketState state,
|
||||||
|
GlobalOptions options,
|
||||||
|
BitgetTradingService trading,
|
||||||
|
PriceLevelTracker levels,
|
||||||
|
ILogger<SignalProcessService> logger)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_options = options;
|
||||||
|
_trading = trading;
|
||||||
|
_levels = levels;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 while(true) { calProcess } 루프의 1회 반복에 해당하는 처리.
|
||||||
|
/// BackgroundService 가 짧은 주기로 호출하도록 설계.
|
||||||
|
/// </summary>
|
||||||
|
public async Task ProcessOnceAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 최초 1회 실행: hedge mode 전환
|
||||||
|
if (!_positionModeInitialized)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _trading.ChangeFuturePositionModeAsync(FuturePositionMode.hedge_mode, ct).ConfigureAwait(false);
|
||||||
|
_positionModeInitialized = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "초기 포지션 모드 변경 실패");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ichimoku = new IchimokuConditionSetup();
|
||||||
|
var movingAverage = new MovingAverageConditionSetup();
|
||||||
|
var nowTime = DateTime.Now;
|
||||||
|
|
||||||
|
// 만료된 신호 정리 (기존 signals_*.ToList() 처리와 동일)
|
||||||
|
CleanupExpiredSignals(nowTime);
|
||||||
|
UpdateActiveSignalPrices();
|
||||||
|
|
||||||
|
if (_state.CheckedMarkets.Count == 0) return;
|
||||||
|
|
||||||
|
// 체크된 마켓별 신호 검색
|
||||||
|
foreach (var marketObj in _state.CheckedMarkets.ToList())
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
var market = marketObj?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(market)) continue;
|
||||||
|
|
||||||
|
MarketCandleDataList findedMarket;
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
findedMarket = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == market);
|
||||||
|
}
|
||||||
|
if (findedMarket == null) continue;
|
||||||
|
|
||||||
|
var enumList = Enum.GetValues(typeof(MCandleTimeType));
|
||||||
|
Array.Reverse(enumList);
|
||||||
|
|
||||||
|
foreach (MCandleTimeType interval in enumList)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
var strInterval = Convertor.GetEnumDescription(interval);
|
||||||
|
if (!_state.IntervalList.Any(x => x?.ToString() == strInterval)) continue;
|
||||||
|
|
||||||
|
// 자정(24:00) 기준 전저고점 초기화 플래그
|
||||||
|
if (nowTime.Minute == 0 && nowTime.Hour == 24 && _전저고점Flag)
|
||||||
|
{
|
||||||
|
_levels.Clear();
|
||||||
|
_전저고점Flag = false;
|
||||||
|
}
|
||||||
|
else if (nowTime.Minute > 0 && nowTime.Hour == 24)
|
||||||
|
{
|
||||||
|
_전저고점Flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전저고점 초기 계산 (최초 진입 시)
|
||||||
|
if (!_levels.ContainsKey(market + strInterval))
|
||||||
|
{
|
||||||
|
InitializePriceLevels(findedMarket, market, strInterval, interval, ichimoku, movingAverage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BuyShortStep.ContainsKey(market))
|
||||||
|
{
|
||||||
|
BuyShortStep[market] = 0;
|
||||||
|
BuyLongStep[market] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존에서는 5m 필수 / 현재봉 평가 로직이 뒤따름
|
||||||
|
if (!_levels.ContainsKey(market + "5m")) continue;
|
||||||
|
|
||||||
|
// 5분봉 처리 체인 (전/전전/전전전/전전전전 봉 지표 계산)
|
||||||
|
EvaluateFivemCandleChain(findedMarket, ichimoku, movingAverage);
|
||||||
|
|
||||||
|
// 전략 실행 (영호 조건식 상/하향 돌파)
|
||||||
|
if (영호조건식_상향돌파 || 영호조건식_하향돌파)
|
||||||
|
{
|
||||||
|
await EvaluateStrategyAsync(market, strInterval, interval, findedMarket, nowTime, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "SignalProcess 처리 중 오류");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupExpiredSignals(DateTime nowTime)
|
||||||
|
{
|
||||||
|
foreach (var item in SignalsLongUp.ToList())
|
||||||
|
if (item.SignalStart + SessionTime < nowTime) SignalsLongUp.Remove(item);
|
||||||
|
foreach (var item in SignalsShortDown.ToList())
|
||||||
|
if (item.SignalStart + SessionTime < nowTime) SignalsShortDown.Remove(item);
|
||||||
|
foreach (var item in SignalsLongDown.ToList())
|
||||||
|
if (item.SignalStart + SessionTime < nowTime) SignalsShortUp.Remove(item);
|
||||||
|
foreach (var item in SignalsShortUp.ToList())
|
||||||
|
if (item.SignalStart + SessionTime < nowTime) SignalsShortDown.Remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateActiveSignalPrices()
|
||||||
|
{
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
foreach (var sig in SignalsLongUp)
|
||||||
|
{
|
||||||
|
var market = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == sig.MarketName);
|
||||||
|
if (market == null) continue;
|
||||||
|
if (!market.candleDataList.TryGetValue(sig.MCandleTimeType, out var list) || list.Count == 0) continue;
|
||||||
|
|
||||||
|
var last = list.OrderBy(i => i.Argument).Last();
|
||||||
|
if (last.IchimokData == null) continue;
|
||||||
|
|
||||||
|
if (Checked구름떼상향돌파 || 영호조건식_상향돌파)
|
||||||
|
{
|
||||||
|
sig.Price = last.Close;
|
||||||
|
sig.Rate = (last.Close / last.Open) * 100 - 100;
|
||||||
|
sig.IndicatorRate = (last.Close / sig.IndicatorPrice) * 100 - 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var sig in SignalsShortDown)
|
||||||
|
{
|
||||||
|
var market = _state.candleDataLists.Find(i => i.ConGoodsData?.MarketName == sig.MarketName);
|
||||||
|
if (market == null) continue;
|
||||||
|
if (!market.candleDataList.TryGetValue(sig.MCandleTimeType, out var list) || list.Count == 0) continue;
|
||||||
|
|
||||||
|
var last = list.OrderBy(i => i.Argument).Last();
|
||||||
|
if (last.IchimokData == null) continue;
|
||||||
|
|
||||||
|
if (Checked구름떼하향돌파 || 영호조건식_하향돌파)
|
||||||
|
{
|
||||||
|
sig.Price = last.Close;
|
||||||
|
sig.Rate = (last.Close / last.Open) * 100 - 100;
|
||||||
|
sig.IndicatorRate = (last.Close / sig.IndicatorPrice) * 100 - 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializePriceLevels(
|
||||||
|
MarketCandleDataList findedMarket,
|
||||||
|
string market,
|
||||||
|
string strInterval,
|
||||||
|
MCandleTimeType interval,
|
||||||
|
IchimokuConditionSetup ichimoku,
|
||||||
|
MovingAverageConditionSetup movingAverage)
|
||||||
|
{
|
||||||
|
_levels.GetOrCreate(market + strInterval);
|
||||||
|
|
||||||
|
List<CandleData> sorted;
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
if (!findedMarket.candleDataList.TryGetValue(interval, out var raw)) return;
|
||||||
|
sorted = raw.OrderBy(i => i.Argument).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sorted.Count < 212) return; // 기존 코드 기준 (199 + 최소 여유)
|
||||||
|
|
||||||
|
var tempList = new List<CandleData>();
|
||||||
|
for (int i = 0; i < 199 && i < sorted.Count; i++) tempList.Add(sorted[i]);
|
||||||
|
|
||||||
|
for (int i = 199; i < sorted.Count; i++)
|
||||||
|
{
|
||||||
|
var main = sorted[i];
|
||||||
|
if (main == null) continue;
|
||||||
|
tempList.Add(main);
|
||||||
|
|
||||||
|
ichimoku.calIchimokuData(tempList, main);
|
||||||
|
movingAverage.Cal_YHK_Downpublic(tempList, main);
|
||||||
|
|
||||||
|
if (i > 210)
|
||||||
|
{
|
||||||
|
var mv4Now = tempList[i].movingAverageDataList[4].Value;
|
||||||
|
var mv4Prev = tempList[i - 1].movingAverageDataList[4].Value;
|
||||||
|
var mv4Prev2 = tempList[i - 2].movingAverageDataList[4].Value;
|
||||||
|
var mv4Prev3 = tempList[i - 3].movingAverageDataList[4].Value;
|
||||||
|
|
||||||
|
_levels.Calculate(market, strInterval, interval, mv4Prev, mv4Prev2, mv4Prev3,
|
||||||
|
tempList[tempList.Count - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EvaluateFivemCandleChain(
|
||||||
|
MarketCandleDataList findedMarket,
|
||||||
|
IchimokuConditionSetup ichimoku,
|
||||||
|
MovingAverageConditionSetup movingAverage)
|
||||||
|
{
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
if (!findedMarket.candleDataList.TryGetValue(MCandleTimeType.M5, out var list) || list.Count < 5) return;
|
||||||
|
var sorted = list.OrderBy(i => i.Argument).ToList();
|
||||||
|
|
||||||
|
for (int back = 0; back < 5; back++)
|
||||||
|
{
|
||||||
|
if (sorted.Count == 0) break;
|
||||||
|
var candle = sorted.Last();
|
||||||
|
if (candle == null) break;
|
||||||
|
|
||||||
|
ichimoku.calIchimokuData(sorted, candle);
|
||||||
|
movingAverage.Cal_YHK_Downpublic(sorted, candle);
|
||||||
|
|
||||||
|
sorted.RemoveAt(sorted.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 영호 조건식 상/하향 돌파 전략 실행 엔트리 포인트.
|
||||||
|
/// 기존 calProcess 의 상승/하락 진입·청산 로직을 실제 주문 호출로 이식한다.
|
||||||
|
/// 복잡도가 매우 높아 기본 진입 지점만 유지, 상세 평가 로직은 기존 WinForms 에서 주석 해제된 부분을 참고해 확장할 것.
|
||||||
|
/// </summary>
|
||||||
|
private async Task EvaluateStrategyAsync(
|
||||||
|
string market,
|
||||||
|
string strInterval,
|
||||||
|
MCandleTimeType interval,
|
||||||
|
MarketCandleDataList findedMarket,
|
||||||
|
DateTime nowTime,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 전저고점 참조
|
||||||
|
var 저고점_5 = _levels.GetOrCreate(market + "5m");
|
||||||
|
if (저고점_5["전전전저가"] == 0 || 저고점_5["전전전고가"] == 0) return;
|
||||||
|
|
||||||
|
CandleData candleData_5;
|
||||||
|
CandleData candleData_5_전봉;
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
if (!findedMarket.candleDataList.TryGetValue(MCandleTimeType.M5, out var list) || list.Count < 2) return;
|
||||||
|
var sorted = list.OrderBy(i => i.Argument).ToList();
|
||||||
|
candleData_5 = sorted[sorted.Count - 1];
|
||||||
|
candleData_5_전봉 = sorted[sorted.Count - 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
var price = candleData_5.Close;
|
||||||
|
|
||||||
|
// === 상향 돌파: Long 진입/청산 (기존 UpBuyList 흐름) ===
|
||||||
|
if (영호조건식_상향돌파 && !UpBuyList.ContainsKey(market))
|
||||||
|
{
|
||||||
|
// 진입 조건: 이전 저점이 상승 전환 (단순화된 기준)
|
||||||
|
if (저고점_5["전저가"] < 저고점_5["전전저가"] && price > candleData_5_전봉.Close)
|
||||||
|
{
|
||||||
|
double size = _options.OrderAmount / price * _options.Leverage;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var orderData = await _trading.FutureOrderAsync(
|
||||||
|
market, FutureMarginMode.isolated, size, price,
|
||||||
|
FutureSide.BuyLong, OrderType.limit, _options.Leverage, ct: ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (orderData != null && orderData.TryGetValue("data", out var dataRaw))
|
||||||
|
{
|
||||||
|
var orderDic = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(
|
||||||
|
Newtonsoft.Json.JsonConvert.SerializeObject(dataRaw));
|
||||||
|
|
||||||
|
var record = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "MarketName", market }, { "BuyDate", nowTime },
|
||||||
|
{ "Price", price }, { "MCandleTimeType", (int)interval },
|
||||||
|
{ "Candle", candleData_5 }, { "Size", size },
|
||||||
|
{ "orderId", orderDic?["orderId"] ?? string.Empty }
|
||||||
|
};
|
||||||
|
UpBuyList[market] = record;
|
||||||
|
BuyLongList.Add(record);
|
||||||
|
LongCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _logger.LogError(ex, "Long 진입 주문 실패: {Market}", market); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 하향 돌파: Short 진입/청산 (기존 DownBuyList 흐름) ===
|
||||||
|
if (영호조건식_하향돌파 && !DownBuyList.ContainsKey(market))
|
||||||
|
{
|
||||||
|
if (저고점_5["전고가"] > 저고점_5["전전고가"] && price < candleData_5_전봉.Close)
|
||||||
|
{
|
||||||
|
double size = _options.OrderAmount / price * _options.Leverage;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var orderData = await _trading.FutureOrderAsync(
|
||||||
|
market, FutureMarginMode.isolated, size, price,
|
||||||
|
FutureSide.BuyShort, OrderType.limit, _options.Leverage, ct: ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (orderData != null && orderData.TryGetValue("data", out var dataRaw))
|
||||||
|
{
|
||||||
|
var orderDic = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(
|
||||||
|
Newtonsoft.Json.JsonConvert.SerializeObject(dataRaw));
|
||||||
|
|
||||||
|
var record = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "MarketName", market }, { "BuyDate", nowTime },
|
||||||
|
{ "Price", price }, { "MCandleTimeType", (int)interval },
|
||||||
|
{ "Candle", candleData_5 }, { "Size", size },
|
||||||
|
{ "orderId", orderDic?["orderId"] ?? string.Empty }
|
||||||
|
};
|
||||||
|
DownBuyList[market] = record;
|
||||||
|
BuyShortList.Add(record);
|
||||||
|
ShortCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _logger.LogError(ex, "Short 진입 주문 실패: {Market}", market); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Bitget_Server/Application/State/MarketState.cs
Normal file
26
Bitget_Server/Application/State/MarketState.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Application.State;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms CommonProperty에서 UI 참조를 제외한 순수 런타임 상태.
|
||||||
|
/// DI 싱글톤으로 등록되어 전 서비스에서 공유.
|
||||||
|
/// </summary>
|
||||||
|
public class MarketState
|
||||||
|
{
|
||||||
|
public ExchangePlace selectedChangePlace { get; set; } = ExchangePlace.Bitget;
|
||||||
|
public PaymentType selectedPaymentType { get; set; } = PaymentType.USDT;
|
||||||
|
public BitgetProductType selectedBitgetProductType { get; set; } = BitgetProductType.FUTURE;
|
||||||
|
|
||||||
|
public List<MarketInfo> marketInfoList { get; set; } = new();
|
||||||
|
public List<MarketCandleDataList> candleDataLists { get; set; } = new();
|
||||||
|
public List<WebSocketTickerInfo> webSocketCandles { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>현재 활성화된 신호 검색에 사용될 마켓/인터벌 목록 (기존 MainForm의 체크박스 값).</summary>
|
||||||
|
public List<object> CheckedMarkets { get; set; } = new();
|
||||||
|
public List<object> IntervalList { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>락 오브젝트 (캔들 리스트 동시 접근 보호).</summary>
|
||||||
|
public readonly object SyncRoot = new();
|
||||||
|
}
|
||||||
@@ -2,21 +2,21 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<RootNamespace>Bitget_Server</RootNamespace>
|
||||||
|
<AssemblyName>Bitget_Server</AssemblyName>
|
||||||
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
</PackageReference>
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
|
<PackageReference Include="Serilog.Sinks.Map" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
9
Bitget_Server/Bitget_Server.csproj.user
Normal file
9
Bitget_Server/Bitget_Server.csproj.user
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ActiveDebugProfile>http</ActiveDebugProfile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bitget_Server.Common
|
|
||||||
{
|
|
||||||
public static class CommonProperty
|
|
||||||
{
|
|
||||||
public static string DbIp { get; set; } = "localhost";
|
|
||||||
|
|
||||||
public static string DbName { get; set; } = "Bitget";
|
|
||||||
public static string DbId { get; set; } = "million";
|
|
||||||
public static string DbPw { get; set; } = "million!";
|
|
||||||
|
|
||||||
public static string EfCoreConnectionStr { get; set; } = $"Server={DbIp};database={DbName};uid={DbId};pwd={DbPw};TrustServerCertificate=true;";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using Bitget_Server.EFCore.Models;
|
|
||||||
using Bitget_Server.EFCore;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Bitget_Server.Controller
|
|
||||||
{
|
|
||||||
|
|
||||||
[ApiController, Route("api")]
|
|
||||||
public class PushAlarmController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly BitgetContext context;
|
|
||||||
|
|
||||||
public PushAlarmController(BitgetContext _context)
|
|
||||||
{
|
|
||||||
context = _context;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet, Route("getalarm")]
|
|
||||||
public List<PushAlarm> GetAlarm(int index)
|
|
||||||
{
|
|
||||||
return context.PushAlarms.Where(x => x.Id > index).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet, Route("setalarm")]
|
|
||||||
public bool SetAlarm(string title,string contents)
|
|
||||||
{
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.PushAlarms.Add(new PushAlarm {Title = title,Contents = contents,AlarmTime = DateTime.Now});
|
|
||||||
context.SaveChanges();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
65
Bitget_Server/Dockerfile
Normal file
65
Bitget_Server/Dockerfile
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Bitget_Server Dockerfile (Ubuntu 24.04 / noble base, .NET 8.0)
|
||||||
|
#
|
||||||
|
# Build context: BitGet_API/ (솔루션 루트)
|
||||||
|
# 예) docker build -f Bitget_Server/Dockerfile -t bitget-server:latest .
|
||||||
|
# 또는 docker-compose up --build
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1) Build stage — SDK 이미지에서 restore/publish 수행
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0-noble AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# restore 캐시 최적화: csproj 만 먼저 복사 → restore → 그 다음 전체 복사
|
||||||
|
COPY ["Bitget_Server/Bitget_Server.csproj", "Bitget_Server/"]
|
||||||
|
RUN dotnet restore "Bitget_Server/Bitget_Server.csproj"
|
||||||
|
|
||||||
|
# 전체 소스 복사
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
WORKDIR "/src/Bitget_Server"
|
||||||
|
RUN dotnet publish "Bitget_Server.csproj" \
|
||||||
|
-c $BUILD_CONFIGURATION \
|
||||||
|
-o /app/publish \
|
||||||
|
/p:UseAppHost=false
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2) Runtime stage — ASP.NET Core 8.0 런타임 (Ubuntu 24.04 noble)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS final
|
||||||
|
|
||||||
|
# Healthcheck 를 위해 curl 설치 (비-root 전환 전에 수행)
|
||||||
|
USER root
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl tzdata \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 타임존 (기본 Asia/Seoul, compose 에서 TZ 환경변수로 덮어쓰기 가능)
|
||||||
|
ENV TZ=Asia/Seoul
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
# 영속화 디렉토리 준비 (compose 에서 볼륨 마운트 대상)
|
||||||
|
RUN mkdir -p /app/data /app/logs && chown -R $APP_UID:$APP_UID /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
RUN chown -R $APP_UID:$APP_UID /app
|
||||||
|
|
||||||
|
# 비-root 사용자로 전환 (aspnet:8.0-noble 는 UID 1654 의 app 사용자 사전 정의)
|
||||||
|
USER $APP_UID
|
||||||
|
|
||||||
|
# Kestrel 포트 강제 (HTTP 전용 5000)
|
||||||
|
ENV ASPNETCORE_HTTP_PORTS=5000
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5000
|
||||||
|
ENV DOTNET_RUNNING_IN_CONTAINER=true
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# 컨테이너 내부 헬스체크 (compose 의 healthcheck 와 이중 안전망)
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD curl -fsS http://localhost:5000/api/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "Bitget_Server.dll"]
|
||||||
46
Bitget_Server/Domain/BitgetEndpoints.cs
Normal file
46
Bitget_Server/Domain/BitgetEndpoints.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
namespace Bitget_Server.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bitget REST API 엔드포인트 URL 상수.
|
||||||
|
/// </summary>
|
||||||
|
public static class BitgetEndpoints
|
||||||
|
{
|
||||||
|
public const string BaseUrl = "https://api.bitget.com";
|
||||||
|
|
||||||
|
public const string MarketInfo_Coin = "https://api.bitget.com/api/v2/spot/public/coins";
|
||||||
|
public const string MarketInfo_Future = "https://api.bitget.com/api/v2/mix/market/tickers?productType=USDT-FUTURES";
|
||||||
|
|
||||||
|
public const string MarketCandle_Coin = "https://api.bitget.com/api/v2/spot/market/candles?symbol={0}&granularity={1}in&startTime={2}&endTime={3}&limit={4}";
|
||||||
|
public const string MarketCandle_Future = "https://api.bitget.com/api/v2/mix/market/candles?symbol={0}&granularity={1}&limit={2}&productType={3}";
|
||||||
|
|
||||||
|
public const string AccountList_Future = "https://api.bitget.com/api/v2/mix/account/accounts?productType=USDT-FUTURES";
|
||||||
|
public const string BuySellOrder_Future = "https://api.bitget.com/api/v2/mix/order/place-order";
|
||||||
|
public const string CancelOrder_Future = "https://api.bitget.com/api/v2/mix/order/cancel-order";
|
||||||
|
public const string PendingOrders_Future = "https://api.bitget.com/api/v2/mix/order/orders-pending?productType=usdt-futures";
|
||||||
|
public const string ChangeLeverage_Future = "https://api.bitget.com/api/v2/mix/account/set-leverage";
|
||||||
|
public const string ChangeMarginMode_Future = "https://api.bitget.com/api/v2/mix/account/set-margin-mode";
|
||||||
|
public const string ChangePositionMode_Future = "https://api.bitget.com/api/v2/mix/account/set-position-mode";
|
||||||
|
public const string OrderDetail_Future = "https://api.bitget.com/api/v2/mix/order/detail";
|
||||||
|
public const string GetTimestamp = "https://api.bitget.com/api/v2/public/time";
|
||||||
|
|
||||||
|
public const string WebSocketPublic = "wss://ws.bitget.com/v2/ws/public";
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 업비트 (미사용 - 레거시 상수)
|
||||||
|
public static class UpbitEndpoints
|
||||||
|
{
|
||||||
|
public const string BaseUrl = "https://api.upbit.com/";
|
||||||
|
public const string MarketInfo = "https://api.upbit.com/v1/market/all";
|
||||||
|
public const string MCandleStick = "https://api.upbit.com/v1/candles/minutes/";
|
||||||
|
public const string DCandleStick = "https://api.upbit.com/v1/candles/days";
|
||||||
|
public const string WCandleStick = "https://api.upbit.com/v1/candles/weeks";
|
||||||
|
public const string MTCandleStick = "https://api.upbit.com/v1/candles/months";
|
||||||
|
public const string Accounts = "https://api.upbit.com/v1/accounts";
|
||||||
|
public const string Ticker = "https://api.upbit.com/v1/ticker";
|
||||||
|
public const string Ticks = "https://api.upbit.com/v1/trades/ticks";
|
||||||
|
public const string Orders = "https://api.upbit.com/v1/orders";
|
||||||
|
public const string Order = "https://api.upbit.com/v1/order";
|
||||||
|
public const string Orderbook = "https://api.upbit.com/v1/orderbook";
|
||||||
|
public const string OrderChance = "https://api.upbit.com/v1/orders/chance";
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
175
Bitget_Server/Domain/Enums/Enums.cs
Normal file
175
Bitget_Server/Domain/Enums/Enums.cs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Domain.Enums;
|
||||||
|
|
||||||
|
public enum WebConnectionType
|
||||||
|
{
|
||||||
|
POST = 0,
|
||||||
|
GET = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ExchangePlace
|
||||||
|
{
|
||||||
|
Upbit = 0,
|
||||||
|
Bithumb = 1,
|
||||||
|
Bitget = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 코인 Payment 타입
|
||||||
|
public enum PaymentType
|
||||||
|
{
|
||||||
|
KRW = 0,
|
||||||
|
BTC = 1,
|
||||||
|
USDT = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최신, 가장오래된, None
|
||||||
|
public enum CandleType
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Old = 1,
|
||||||
|
New = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitget FUTURE 코인 타입
|
||||||
|
public enum BitgetProductType
|
||||||
|
{
|
||||||
|
COIN = 0,
|
||||||
|
FUTURE = 1,
|
||||||
|
MARGIN_보류 = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 거래소 Web 요청 타입
|
||||||
|
public enum ExchangeWebReqType
|
||||||
|
{
|
||||||
|
[Description("/Public/CandleStick")]
|
||||||
|
BithumbCandleStick = 0,
|
||||||
|
[Description("/public/ticker")]
|
||||||
|
BithumbCandleTicker = 1,
|
||||||
|
[Description("/public/Orderbook")]
|
||||||
|
BithumbMarket = 2,
|
||||||
|
|
||||||
|
[Description("UpbitCandle")]
|
||||||
|
UpbitCandleStick = 10,
|
||||||
|
[Description("UpbitMarket")]
|
||||||
|
UpbitMarket = 11,
|
||||||
|
|
||||||
|
[Description("BitgetMarketInfo_SPOT")]
|
||||||
|
BitgetMarket_Spot = 20,
|
||||||
|
[Description("BitgetMarketInfo_FUTURE")]
|
||||||
|
BitgetMarket_Future = 21,
|
||||||
|
[Description("BitgetMCandle_COIN")]
|
||||||
|
BitgetCandleStick_Coin = 22,
|
||||||
|
[Description("BitgetMCandle_FUTURE")]
|
||||||
|
BitgetCandleStick_Future = 23,
|
||||||
|
[Description("BitgetAccountList_FUTURE")]
|
||||||
|
BitgetAccountList_Future = 30,
|
||||||
|
[Description("BitgetOrder_FUTURE")]
|
||||||
|
BitgetOrder_Future = 31,
|
||||||
|
[Description("BitgetOrderCancel_FUTURE")]
|
||||||
|
BitgetCancelOrder_Future = 32,
|
||||||
|
[Description("BitgetPendingOrder_FUTURE")]
|
||||||
|
BitgetPendingOrder_Future = 33,
|
||||||
|
[Description("BitgetChangeLeverage_FUTURE")]
|
||||||
|
BitgetChangeLeverage_Future = 34,
|
||||||
|
[Description("BitgetChangeMarginMode_FUTURE")]
|
||||||
|
BitgetChangeMarginMode_Future = 35,
|
||||||
|
[Description("BitgetChangePositionMode_FUTURE")]
|
||||||
|
BitgetChangePositionMode_Future = 36,
|
||||||
|
[Description("BitgetOrderDetail_FUTURE")]
|
||||||
|
BitgetOrderDetail_Future = 37,
|
||||||
|
[Description("BitgetGetTimeStamp")]
|
||||||
|
BitgetGetTimeStamp = 38
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitget WebSocketType
|
||||||
|
public enum BitgetWebSocketType
|
||||||
|
{
|
||||||
|
현황정보_전체종목 = 0,
|
||||||
|
캔들정보 = 1,
|
||||||
|
보유List = 2,
|
||||||
|
주문하기 = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Side
|
||||||
|
{
|
||||||
|
[Description("buy")]
|
||||||
|
buy = 0,
|
||||||
|
[Description("sell")]
|
||||||
|
sell = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FuturePositionMode
|
||||||
|
{
|
||||||
|
[Description("one_way_mode")]
|
||||||
|
one_way_mode = 0,
|
||||||
|
[Description("hedge_mode")]
|
||||||
|
hedge_mode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OrderType
|
||||||
|
{
|
||||||
|
[Description("limit")]
|
||||||
|
limit = 0,
|
||||||
|
[Description("market")]
|
||||||
|
market = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FutureMarginMode
|
||||||
|
{
|
||||||
|
[Description("isolated")]
|
||||||
|
isolated = 0,
|
||||||
|
[Description("crossed")]
|
||||||
|
crossed = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FutureSide
|
||||||
|
{
|
||||||
|
BuyShort = 0,
|
||||||
|
BuyLong = 1,
|
||||||
|
SellShort = 2,
|
||||||
|
SellLong = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시가/고가/종가/저가
|
||||||
|
public enum CandlePriceType
|
||||||
|
{
|
||||||
|
시가 = 1,
|
||||||
|
고가 = 2,
|
||||||
|
저가 = 3,
|
||||||
|
종가 = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캔들 데이터 간격
|
||||||
|
public enum MCandleTimeType
|
||||||
|
{
|
||||||
|
[Description("3m")]
|
||||||
|
M3 = 3,
|
||||||
|
[Description("5m")]
|
||||||
|
M5 = 5,
|
||||||
|
[Description("1m")]
|
||||||
|
M1 = 1,
|
||||||
|
[Description("15m")]
|
||||||
|
M15 = 15,
|
||||||
|
[Description("30m")]
|
||||||
|
M30 = 30,
|
||||||
|
[Description("1H")]
|
||||||
|
H1 = 60,
|
||||||
|
[Description("2H")]
|
||||||
|
H2 = 120,
|
||||||
|
[Description("3H")]
|
||||||
|
H3 = 180,
|
||||||
|
[Description("4H")]
|
||||||
|
H4 = 240,
|
||||||
|
[Description("6H")]
|
||||||
|
H6 = 360,
|
||||||
|
[Description("12H")]
|
||||||
|
H12 = 720
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이동평균선 종류
|
||||||
|
public enum MovingAverageType
|
||||||
|
{
|
||||||
|
캔들 = 1,
|
||||||
|
거래량 = 2
|
||||||
|
}
|
||||||
21
Bitget_Server/Domain/Indicators/IchimokData.cs
Normal file
21
Bitget_Server/Domain/Indicators/IchimokData.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Bitget_Server.Domain.Indicators;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class IchimokData
|
||||||
|
{
|
||||||
|
public double 기준선 { get; set; } = 0;
|
||||||
|
public double 전환선 { get; set; } = 0;
|
||||||
|
public double 후행스팬 { get; set; } = 0;
|
||||||
|
public double 선행스팬1 { get; set; } = 0;
|
||||||
|
public double 선행스팬2 { get; set; } = 0;
|
||||||
|
|
||||||
|
public double 상한선 { get; set; }
|
||||||
|
public double 하한선 { get; set; }
|
||||||
|
|
||||||
|
public bool IsLongUp { get; set; } = false;
|
||||||
|
public bool IsLongDown { get; set; } = false;
|
||||||
|
public bool IsShortUp { get; set; } = false;
|
||||||
|
public bool IsShortDown { get; set; } = false;
|
||||||
|
|
||||||
|
public bool IsUsedData { get; set; } = true;
|
||||||
|
}
|
||||||
8
Bitget_Server/Domain/Indicators/ImpulseMacdData.cs
Normal file
8
Bitget_Server/Domain/Indicators/ImpulseMacdData.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Bitget_Server.Domain.Indicators;
|
||||||
|
|
||||||
|
public class ImpulseMacdData
|
||||||
|
{
|
||||||
|
public double MD;
|
||||||
|
public double SH;
|
||||||
|
public double SB;
|
||||||
|
}
|
||||||
14
Bitget_Server/Domain/Indicators/MovingAverageData.cs
Normal file
14
Bitget_Server/Domain/Indicators/MovingAverageData.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Bitget_Server.Domain.Indicators;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class MovingAverageData
|
||||||
|
{
|
||||||
|
// MV Value
|
||||||
|
public double Value { get; set; }
|
||||||
|
// Bol Value
|
||||||
|
public double BolangerValue_Down { get; set; }
|
||||||
|
public double Average { get; set; }
|
||||||
|
public double BolangerValue_Up { get; set; }
|
||||||
|
// Bol 편차
|
||||||
|
public double BolangerSquared { get; set; } = 2.5;
|
||||||
|
}
|
||||||
20
Bitget_Server/Domain/Models/ApiKeyInfo.cs
Normal file
20
Bitget_Server/Domain/Models/ApiKeyInfo.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 공통 API 키 정보.
|
||||||
|
/// </summary>
|
||||||
|
public class ApiKeyInfo
|
||||||
|
{
|
||||||
|
public string AccessKey { get; set; }
|
||||||
|
public string SecretKey { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bitget 전용 API 키 정보 (Passphrase 포함).
|
||||||
|
/// </summary>
|
||||||
|
public class ApiKeyInfo_Bitget : ApiKeyInfo
|
||||||
|
{
|
||||||
|
public string PassphraseKey { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
42
Bitget_Server/Domain/Models/CandleData.cs
Normal file
42
Bitget_Server/Domain/Models/CandleData.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Bitget_Server.Domain.Indicators;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
public class CandleData
|
||||||
|
{
|
||||||
|
public DateTime Argument { get; set; }
|
||||||
|
public double Low { get; set; }
|
||||||
|
public double High { get; set; }
|
||||||
|
public double Open { get; set; }
|
||||||
|
public double Close { get; set; }
|
||||||
|
// 기본 통화 거래량 (BTC)
|
||||||
|
public double Vol_C { get; set; }
|
||||||
|
// 견적 통화 거래량 USD
|
||||||
|
public double Vol_USD { get; set; }
|
||||||
|
// USDT 거래량
|
||||||
|
public double Vol_USDT { get; set; }
|
||||||
|
|
||||||
|
// 웹소켓 업데이트 기록 여부
|
||||||
|
public bool WebSocketUpdate { get; set; } = false;
|
||||||
|
// 웹 서비스 업데이트 기록 여부
|
||||||
|
public bool WebServiceUpdate { get; set; } = false;
|
||||||
|
|
||||||
|
public CandleData() { }
|
||||||
|
|
||||||
|
public CandleData(DateTime argument, double low, double high, double open, double close,
|
||||||
|
double vol_C, double vol_USD, double vol_USDT)
|
||||||
|
{
|
||||||
|
this.Argument = argument;
|
||||||
|
this.Low = low;
|
||||||
|
this.High = high;
|
||||||
|
this.Open = open;
|
||||||
|
this.Close = close;
|
||||||
|
this.Vol_C = vol_C;
|
||||||
|
this.Vol_USD = vol_USD;
|
||||||
|
this.Vol_USDT = vol_USDT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IchimokData IchimokData { get; set; }
|
||||||
|
public Dictionary<int, MovingAverageData> movingAverageDataList { get; set; }
|
||||||
|
public Dictionary<int, ImpulseMacdData> impulseMacdDataList { get; set; }
|
||||||
|
}
|
||||||
16
Bitget_Server/Domain/Models/MarketCandleDataList.cs
Normal file
16
Bitget_Server/Domain/Models/MarketCandleDataList.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class MarketCandleDataList
|
||||||
|
{
|
||||||
|
// 코인 종목
|
||||||
|
public MarketInfo ConGoodsData { get; set; } = new MarketInfo();
|
||||||
|
|
||||||
|
// 캔들 데이터 (시간 타입별)
|
||||||
|
public Dictionary<MCandleTimeType, List<CandleData>> candleDataList { get; set; }
|
||||||
|
= new Dictionary<MCandleTimeType, List<CandleData>>();
|
||||||
|
|
||||||
|
public string Uuid { get; set; }
|
||||||
|
}
|
||||||
14
Bitget_Server/Domain/Models/MarketInfo.cs
Normal file
14
Bitget_Server/Domain/Models/MarketInfo.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
public class MarketInfo
|
||||||
|
{
|
||||||
|
public string ExchangePlace { get; set; }
|
||||||
|
public string MarketName { get; set; }
|
||||||
|
public double lastPr { get; set; }
|
||||||
|
public double change24h { get; set; }
|
||||||
|
public double baseVolume { get; set; }
|
||||||
|
public double quoteVolume { get; set; }
|
||||||
|
public double high24h { get; set; }
|
||||||
|
public double low24h { get; set; }
|
||||||
|
public double open24h { get; set; }
|
||||||
|
}
|
||||||
21
Bitget_Server/Domain/Models/ResponseCandleStick.cs
Normal file
21
Bitget_Server/Domain/Models/ResponseCandleStick.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
public class ResponseCandleStick
|
||||||
|
{
|
||||||
|
public string action { get; set; }
|
||||||
|
public ResponseCandleStickArg arg { get; set; }
|
||||||
|
public List<string> data { get; set; }
|
||||||
|
public double ts { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTime reciveTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResponseCandleStickArg
|
||||||
|
{
|
||||||
|
public string instType { get; set; }
|
||||||
|
public string channel { get; set; }
|
||||||
|
public string instId { get; set; }
|
||||||
|
}
|
||||||
23
Bitget_Server/Domain/Models/SignalInfoData.cs
Normal file
23
Bitget_Server/Domain/Models/SignalInfoData.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
public class SignalInfoData
|
||||||
|
{
|
||||||
|
// 신호검색 시작 시간
|
||||||
|
public DateTime SignalStart { get; set; }
|
||||||
|
// 종목 이름
|
||||||
|
public string MarketName { get; set; }
|
||||||
|
// 종목 설명
|
||||||
|
public string Description { get; set; }
|
||||||
|
// 현재가
|
||||||
|
public double Price { get; set; }
|
||||||
|
// 설정된 시간 봉의 수익율
|
||||||
|
public double Rate { get; set; }
|
||||||
|
// 구름떼 상한선 가격
|
||||||
|
public double IndicatorPrice { get; set; }
|
||||||
|
// 설정된 지표(Indicator)와의 격차 비율
|
||||||
|
public double IndicatorRate { get; set; }
|
||||||
|
|
||||||
|
public MCandleTimeType MCandleTimeType { get; set; }
|
||||||
|
}
|
||||||
19
Bitget_Server/Domain/Models/TradeData.cs
Normal file
19
Bitget_Server/Domain/Models/TradeData.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
public class TradeData
|
||||||
|
{
|
||||||
|
public string type { get; set; }
|
||||||
|
public string code { get; set; }
|
||||||
|
public long timestamp { get; set; }
|
||||||
|
public string trade_date { get; set; }
|
||||||
|
public string trade_time { get; set; }
|
||||||
|
public long trade_timestamp { get; set; }
|
||||||
|
public double trade_price { get; set; }
|
||||||
|
public double trade_volume { get; set; }
|
||||||
|
public string ask_bid { get; set; }
|
||||||
|
public double prev_closing_price { get; set; }
|
||||||
|
public string change { get; set; }
|
||||||
|
public double change_price { get; set; }
|
||||||
|
public long sequential_id { get; set; }
|
||||||
|
public string stream_type { get; set; }
|
||||||
|
}
|
||||||
29
Bitget_Server/Domain/Models/WebSocketTickerInfo.cs
Normal file
29
Bitget_Server/Domain/Models/WebSocketTickerInfo.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace Bitget_Server.Domain.Models;
|
||||||
|
|
||||||
|
public class WebSocketTickerInfo
|
||||||
|
{
|
||||||
|
// 종목
|
||||||
|
public string instId { get; set; }
|
||||||
|
// 최종가격
|
||||||
|
public double lastPr { get; set; }
|
||||||
|
// 24시간 시가
|
||||||
|
public double open24h { get; set; }
|
||||||
|
// 24시간 고가
|
||||||
|
public double high24h { get; set; }
|
||||||
|
// 24시간 저가
|
||||||
|
public double low24h { get; set; }
|
||||||
|
// 수익률
|
||||||
|
public double change24h { get; set; }
|
||||||
|
public double bidPr { get; set; }
|
||||||
|
public double askPr { get; set; }
|
||||||
|
public double bidSz { get; set; }
|
||||||
|
public double askSz { get; set; }
|
||||||
|
// 코인 거래량
|
||||||
|
public double baseVolume { get; set; }
|
||||||
|
// 견적통화 거래량
|
||||||
|
public double quoteVolume { get; set; }
|
||||||
|
public double openUtc { get; set; }
|
||||||
|
public double changeUtc24h { get; set; }
|
||||||
|
public long ts { get; set; }
|
||||||
|
public DateTime reciveTime { get; set; }
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using Bitget_Server.Common;
|
|
||||||
using Bitget_Server.EFCore.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bitget_Server.EFCore
|
|
||||||
{
|
|
||||||
public class BitgetContext : DbContext
|
|
||||||
{
|
|
||||||
|
|
||||||
public DbSet<PushAlarm> PushAlarms { get; set; }
|
|
||||||
|
|
||||||
public BitgetContext(DbContextOptions<BitgetContext> options)
|
|
||||||
: base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
{
|
|
||||||
optionsBuilder.UseSqlServer(CommonProperty.EfCoreConnectionStr);
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bitget_Server.EFCore.Models
|
|
||||||
{
|
|
||||||
public class PushAlarm
|
|
||||||
{
|
|
||||||
|
|
||||||
[Key]
|
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
public DateTime AlarmTime { get; set; }
|
|
||||||
|
|
||||||
public string Title { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string Contents { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
277
Bitget_Server/Infrastructure/Bitget/BitgetClient.cs
Normal file
277
Bitget_Server/Infrastructure/Bitget/BitgetClient.cs
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Bitget_Server.Infrastructure.Utils;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Infrastructure.Bitget;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms WebConnection.SendWebServer / GenerateSignString 로직을 이식.
|
||||||
|
/// HttpClient 기반으로 재작성하고, 모든 외부 호출 전에 BitgetRateLimiter를 거친다.
|
||||||
|
/// </summary>
|
||||||
|
public class BitgetClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private readonly BitgetRateLimiter _limiter;
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly ILogger<BitgetClient> _logger;
|
||||||
|
|
||||||
|
public BitgetClient(
|
||||||
|
HttpClient http,
|
||||||
|
BitgetRateLimiter limiter,
|
||||||
|
MarketState state,
|
||||||
|
ILogger<BitgetClient> logger)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
_limiter = limiter;
|
||||||
|
_state = state;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WebConnection.SendWebServer와 동일한 시그니처를 유지한다.
|
||||||
|
/// 응답을 ExchangeWebReqType에 따라 적절히 역직렬화하여 반환한다.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> SendWebServerAsync(
|
||||||
|
ExchangeWebReqType exchangeWebReqType,
|
||||||
|
string url = "",
|
||||||
|
MarketCandleDataList marketCandleDataList = null,
|
||||||
|
MCandleTimeType interval = MCandleTimeType.M1,
|
||||||
|
string postData = "",
|
||||||
|
string subUri = "",
|
||||||
|
NameValueCollection headers = null,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var requestUrl = string.IsNullOrEmpty(url) ? GetDefaultUrl(exchangeWebReqType) : url;
|
||||||
|
if (string.IsNullOrEmpty(requestUrl))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("URL 미정 ExchangeWebReqType: {Type}", exchangeWebReqType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1차 호출 → 429 시 1회 재시도
|
||||||
|
for (int attempt = 0; attempt < 2; attempt++)
|
||||||
|
{
|
||||||
|
await _limiter.AcquireAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpRequestMessage req;
|
||||||
|
if (!string.IsNullOrEmpty(postData))
|
||||||
|
{
|
||||||
|
req = new HttpRequestMessage(HttpMethod.Post, requestUrl)
|
||||||
|
{
|
||||||
|
Content = new StringContent(postData, Encoding.UTF8,
|
||||||
|
headers != null ? "application/json" : "application/x-www-form-urlencoded")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
req = new HttpRequestMessage(HttpMethod.Get, requestUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers != null)
|
||||||
|
{
|
||||||
|
foreach (string key in headers)
|
||||||
|
req.Headers.TryAddWithoutValidation(key, headers[key]);
|
||||||
|
|
||||||
|
if (req.Content != null)
|
||||||
|
req.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
|
var resResult = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 429 (TooManyRequests) 는 1회에 한해 500ms 후 재시도
|
||||||
|
if ((int)resp.StatusCode == 429 && attempt == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Bitget 429 TooManyRequests — 500ms 후 1회 재시도 ({Type})", exchangeWebReqType);
|
||||||
|
await Task.Delay(500, ct).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError("Bitget API 에러 {Status}: {Body}", (int)resp.StatusCode, resResult);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parse(exchangeWebReqType, resResult, marketCandleDataList, interval);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException webEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(webEx, "Bitget HTTP 호출 실패: {Type}", exchangeWebReqType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Bitget 처리 중 예외");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDefaultUrl(ExchangeWebReqType type) => type switch
|
||||||
|
{
|
||||||
|
ExchangeWebReqType.UpbitMarket => UpbitEndpoints.MarketInfo,
|
||||||
|
ExchangeWebReqType.BitgetMarket_Spot => BitgetEndpoints.MarketInfo_Coin,
|
||||||
|
ExchangeWebReqType.BitgetMarket_Future => BitgetEndpoints.MarketInfo_Future,
|
||||||
|
ExchangeWebReqType.BitgetCandleStick_Coin => BitgetEndpoints.MarketCandle_Coin,
|
||||||
|
ExchangeWebReqType.BitgetCandleStick_Future => BitgetEndpoints.MarketCandle_Future,
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
private object Parse(ExchangeWebReqType type, string resResult, MarketCandleDataList marketCandleDataList, MCandleTimeType interval)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ExchangeWebReqType.BitgetMarket_Spot:
|
||||||
|
{
|
||||||
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
|
var marketInfo = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
||||||
|
return marketInfo.FindAll(x => x["coin"].ToString().Contains("USDT"));
|
||||||
|
}
|
||||||
|
case ExchangeWebReqType.BitgetMarket_Future:
|
||||||
|
{
|
||||||
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
|
var marketInfos = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
||||||
|
|
||||||
|
var newList = new List<MarketInfo>();
|
||||||
|
foreach (var item in marketInfos)
|
||||||
|
{
|
||||||
|
var marketInfo = new MarketInfo
|
||||||
|
{
|
||||||
|
MarketName = item["symbol"].ToString(),
|
||||||
|
ExchangePlace = Enum.GetName(typeof(ExchangePlace), _state.selectedChangePlace),
|
||||||
|
lastPr = Convert.ToDouble(item["lastPr"]),
|
||||||
|
change24h = Convert.ToDouble(item["change24h"]),
|
||||||
|
baseVolume = Convert.ToDouble(item["baseVolume"]),
|
||||||
|
quoteVolume = Convert.ToDouble(item["quoteVolume"]),
|
||||||
|
high24h = Convert.ToDouble(item["high24h"]),
|
||||||
|
low24h = Convert.ToDouble(item["low24h"]),
|
||||||
|
open24h = Convert.ToDouble(item["open24h"])
|
||||||
|
};
|
||||||
|
newList.Add(marketInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_state.SyncRoot) { _state.marketInfoList = newList; }
|
||||||
|
return newList;
|
||||||
|
}
|
||||||
|
case ExchangeWebReqType.BitgetCandleStick_Future:
|
||||||
|
{
|
||||||
|
if (marketCandleDataList == null) return null;
|
||||||
|
|
||||||
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
|
var datas = JsonConvert.DeserializeObject<List<List<object>>>(datalist["data"]?.ToString());
|
||||||
|
|
||||||
|
var candleDataList = marketCandleDataList.candleDataList[interval];
|
||||||
|
|
||||||
|
lock (_state.SyncRoot)
|
||||||
|
{
|
||||||
|
foreach (var item in datas)
|
||||||
|
{
|
||||||
|
var candleData = new CandleData
|
||||||
|
{
|
||||||
|
Argument = Convertor.GetUnixtimeStampToDateTime(long.Parse(item[0].ToString())),
|
||||||
|
Open = Convert.ToDouble(item[1]),
|
||||||
|
High = Convert.ToDouble(item[2]),
|
||||||
|
Low = Convert.ToDouble(item[3]),
|
||||||
|
Close = Convert.ToDouble(item[4]),
|
||||||
|
Vol_C = Convert.ToDouble(item[5]),
|
||||||
|
Vol_USD = Convert.ToDouble(item[6])
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item == datas.Last())
|
||||||
|
{
|
||||||
|
var span = GetIntervalSpan(interval);
|
||||||
|
var same = candleDataList.Find(i =>
|
||||||
|
candleData.Argument <= i.Argument &&
|
||||||
|
i.Argument < (candleData.Argument + span) &&
|
||||||
|
i.WebSocketUpdate);
|
||||||
|
|
||||||
|
if (same != null)
|
||||||
|
{
|
||||||
|
same.Open = candleData.Open;
|
||||||
|
same.Low = Math.Min(same.Low, candleData.Low);
|
||||||
|
same.High = Math.Max(same.High, candleData.High);
|
||||||
|
same.WebServiceUpdate = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
candleDataList.Add(candleData);
|
||||||
|
candleData.WebServiceUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
candleDataList.Add(candleData);
|
||||||
|
candleData.WebServiceUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ExchangeWebReqType.BitgetAccountList_Future:
|
||||||
|
{
|
||||||
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
|
return JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
|
||||||
|
}
|
||||||
|
case ExchangeWebReqType.BitgetOrder_Future:
|
||||||
|
case ExchangeWebReqType.BitgetCancelOrder_Future:
|
||||||
|
case ExchangeWebReqType.BitgetChangeLeverage_Future:
|
||||||
|
case ExchangeWebReqType.BitgetChangeMarginMode_Future:
|
||||||
|
case ExchangeWebReqType.BitgetChangePositionMode_Future:
|
||||||
|
case ExchangeWebReqType.BitgetOrderDetail_Future:
|
||||||
|
case ExchangeWebReqType.BitgetGetTimeStamp:
|
||||||
|
case ExchangeWebReqType.BitgetPendingOrder_Future:
|
||||||
|
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan GetIntervalSpan(MCandleTimeType interval) => interval switch
|
||||||
|
{
|
||||||
|
MCandleTimeType.M1 => TimeSpan.FromMinutes(1),
|
||||||
|
MCandleTimeType.M3 => TimeSpan.FromMinutes(3),
|
||||||
|
MCandleTimeType.M5 => TimeSpan.FromMinutes(5),
|
||||||
|
MCandleTimeType.M15 => TimeSpan.FromMinutes(15),
|
||||||
|
MCandleTimeType.M30 => TimeSpan.FromMinutes(30),
|
||||||
|
MCandleTimeType.H1 => TimeSpan.FromHours(1),
|
||||||
|
MCandleTimeType.H2 => TimeSpan.FromHours(2),
|
||||||
|
MCandleTimeType.H3 => TimeSpan.FromHours(3),
|
||||||
|
MCandleTimeType.H4 => TimeSpan.FromHours(4),
|
||||||
|
MCandleTimeType.H6 => TimeSpan.FromHours(6),
|
||||||
|
MCandleTimeType.H12 => TimeSpan.FromHours(12),
|
||||||
|
_ => TimeSpan.Zero
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 IndicatorProcess.GenerateSignString을 그대로 이식.
|
||||||
|
/// </summary>
|
||||||
|
public static string GenerateSignString(string timestamp, string method, string requestPath, string queryString, string body, string secretKey)
|
||||||
|
{
|
||||||
|
method = method.ToUpper();
|
||||||
|
body = body == null ? "" : body;
|
||||||
|
queryString = string.IsNullOrWhiteSpace(queryString) ? "" : "?" + queryString;
|
||||||
|
string preHash = timestamp + method + requestPath + queryString + body;
|
||||||
|
|
||||||
|
var encSecretKey_byteData = Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
var encPreHash_byteData = Encoding.UTF8.GetBytes(preHash);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(new HMACSHA256(encSecretKey_byteData).ComputeHash(encPreHash_byteData));
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Bitget_Server/Infrastructure/Bitget/BitgetRateLimiter.cs
Normal file
71
Bitget_Server/Infrastructure/Bitget/BitgetRateLimiter.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
namespace Bitget_Server.Infrastructure.Bitget;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bitget REST API에 대한 클라이언트 사이드 Rate Limiter.
|
||||||
|
/// Sliding window 방식으로 1초당 N회 호출 제한을 강제한다.
|
||||||
|
/// 모든 외부 호출은 await AcquireAsync(...) 후에 수행되어야 한다.
|
||||||
|
/// </summary>
|
||||||
|
public class BitgetRateLimiter
|
||||||
|
{
|
||||||
|
private readonly int _maxRequestsPerSecond;
|
||||||
|
private readonly Queue<long> _requestTicks = new();
|
||||||
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||||
|
private readonly ILogger<BitgetRateLimiter> _logger;
|
||||||
|
|
||||||
|
public BitgetRateLimiter(int maxRequestsPerSecond, ILogger<BitgetRateLimiter> logger)
|
||||||
|
{
|
||||||
|
// 0 또는 음수 → 무제한 모드. 양수 → 슬라이딩 윈도우 적용.
|
||||||
|
_maxRequestsPerSecond = maxRequestsPerSecond <= 0 ? int.MaxValue : maxRequestsPerSecond;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
if (_maxRequestsPerSecond == int.MaxValue)
|
||||||
|
_logger.LogWarning("BitgetRateLimiter: RateLimitPerSecond <= 0 — 무제한 모드로 동작합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 호출 슬롯이 확보될 때까지 대기한 뒤 즉시 반환한다.
|
||||||
|
/// 무제한 모드(_maxRequestsPerSecond == int.MaxValue) 에서는 즉시 반환.
|
||||||
|
/// </summary>
|
||||||
|
public async Task AcquireAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (_maxRequestsPerSecond == int.MaxValue) return;
|
||||||
|
|
||||||
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int totalWaitedMs = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var now = Environment.TickCount64;
|
||||||
|
// 1초 이전 요청 기록 제거
|
||||||
|
while (_requestTicks.Count > 0 && now - _requestTicks.Peek() >= 1000)
|
||||||
|
_requestTicks.Dequeue();
|
||||||
|
|
||||||
|
if (_requestTicks.Count < _maxRequestsPerSecond)
|
||||||
|
{
|
||||||
|
_requestTicks.Enqueue(now);
|
||||||
|
if (totalWaitedMs >= 100)
|
||||||
|
_logger.LogInformation("Rate limit throttled {WaitMs}ms (queue={Count}/{Max})",
|
||||||
|
totalWaitedMs, _requestTicks.Count, _maxRequestsPerSecond);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 가장 오래된 요청이 1초 경과할 때까지 대기
|
||||||
|
var waitMs = 1000 - (int)(now - _requestTicks.Peek()) + 1;
|
||||||
|
if (waitMs < 1) waitMs = 1;
|
||||||
|
totalWaitedMs += waitMs;
|
||||||
|
|
||||||
|
_logger.LogDebug("Rate limit hit. Waiting {WaitMs}ms (queue={Count}/{Max})",
|
||||||
|
waitMs, _requestTicks.Count, _maxRequestsPerSecond);
|
||||||
|
|
||||||
|
_gate.Release();
|
||||||
|
try { await Task.Delay(waitMs, ct).ConfigureAwait(false); }
|
||||||
|
finally { await _gate.WaitAsync(ct).ConfigureAwait(false); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
237
Bitget_Server/Infrastructure/Bitget/BitgetWebSocketClient.cs
Normal file
237
Bitget_Server/Infrastructure/Bitget/BitgetWebSocketClient.cs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using Bitget_Server.Application.Options;
|
||||||
|
using Bitget_Server.Application.State;
|
||||||
|
using Bitget_Server.Domain;
|
||||||
|
using Bitget_Server.Domain.Enums;
|
||||||
|
using Bitget_Server.Domain.Models;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Infrastructure.Bitget;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 WinForms WebConnection의 WebSocket 로직(BitGetWebSocektStart, closeWebSocket, ContinuePing,
|
||||||
|
/// ValidateJSON, CalculateDataList)을 ASP.NET Core 환경으로 이식.
|
||||||
|
/// 싱글톤으로 등록되어 백그라운드 서비스 + Controller에서 공유됨.
|
||||||
|
/// </summary>
|
||||||
|
public class BitgetWebSocketClient : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly MarketState _state;
|
||||||
|
private readonly ILogger<BitgetWebSocketClient> _logger;
|
||||||
|
private readonly BitgetOptions _options;
|
||||||
|
|
||||||
|
private ClientWebSocket _webSocket;
|
||||||
|
private CancellationTokenSource _cts;
|
||||||
|
private Task _receiveLoopTask;
|
||||||
|
private Task _pingLoopTask;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly List<Dictionary<string, object>> _webSocketDataList = new();
|
||||||
|
|
||||||
|
public bool IsConnected =>
|
||||||
|
_webSocket != null && _webSocket.State == WebSocketState.Open;
|
||||||
|
|
||||||
|
public BitgetWebSocketClient(
|
||||||
|
MarketState state,
|
||||||
|
IOptions<BitgetOptions> options,
|
||||||
|
ILogger<BitgetWebSocketClient> logger)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 BitGetWebSocektStart와 동일 — subscribe 메시지(dic)를 만들어 연결 시작.
|
||||||
|
/// </summary>
|
||||||
|
public async Task StartAsync(BitgetWebSocketType wsType, Dictionary<string, object> subscribeDic, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await StopAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
_webSocket = new ClientWebSocket();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _webSocket.ConnectAsync(new Uri(BitgetEndpoints.WebSocketPublic), _cts.Token).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("WebSocket 연결 성공 ({Type})", wsType);
|
||||||
|
|
||||||
|
var jsonData = JsonConvert.SerializeObject(subscribeDic);
|
||||||
|
await SendMessageAsync(jsonData, _cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_pingLoopTask = Task.Run(() => PingLoopAsync(_cts.Token));
|
||||||
|
_receiveLoopTask = Task.Run(() => ReceiveLoopAsync(_cts.Token));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WebSocket 연결 실패");
|
||||||
|
await StopAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var tempStrData = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested && _webSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), ct).ConfigureAwait(false);
|
||||||
|
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||||
|
|
||||||
|
if (message.Contains("pong"))
|
||||||
|
{
|
||||||
|
var parts = message.Split(new[] { "pong" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
message = string.Concat(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 조각 병합
|
||||||
|
if (tempStrData.Length == 0)
|
||||||
|
{
|
||||||
|
if (!ValidateJSON(message)) { tempStrData = message; continue; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tempStrData += message;
|
||||||
|
if (!ValidateJSON(tempStrData)) continue;
|
||||||
|
message = tempStrData;
|
||||||
|
tempStrData = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Contains("{\"action"))
|
||||||
|
{
|
||||||
|
var splitData = message.Split(new[] { "{\"action" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var item in splitData)
|
||||||
|
{
|
||||||
|
var packet = "{\"action" + item;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(packet);
|
||||||
|
if (datalist != null && datalist.TryGetValue("action", out var action) && action?.ToString() == "update")
|
||||||
|
{
|
||||||
|
lock (_lock) _webSocketDataList.Add(datalist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonReaderException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WebSocket 수신 루프 오류");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PingLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(_options.WebSocketPingIntervalMs, ct).ConfigureAwait(false);
|
||||||
|
await SendMessageAsync("ping", ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Ping 루프 종료");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendMessageAsync(string message, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(message);
|
||||||
|
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, ct).ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("WebSocket 메시지 전송: {Message}", message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "WebSocket 메시지 전송 실패");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ValidateJSON(string s)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (s.Contains("{\"action"))
|
||||||
|
{
|
||||||
|
var splitData = s.Split(new[] { "{\"action" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var item in splitData)
|
||||||
|
{
|
||||||
|
try { JToken.Parse("{\"action" + item); }
|
||||||
|
catch (JsonReaderException) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
JToken.Parse(s);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonReaderException) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기존 CalculateDataList 로직 이식.
|
||||||
|
/// BackgroundService가 1초 주기로 호출하여 WebSocket 누적 데이터를 candleDataLists로 병합.
|
||||||
|
/// </summary>
|
||||||
|
public void CalculateDataList(BitgetWebSocketType bitgetWebSocketType, List<object> intervalList)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<Dictionary<string, object>> cloneList;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
cloneList = _webSocketDataList.Select(d => new Dictionary<string, object>(d)).ToList();
|
||||||
|
_webSocketDataList.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 주의: 복잡한 캔들 병합 로직은 기존 WinForms 버전 그대로 유지 시
|
||||||
|
// IndicatorProcess와 강하게 결합되므로 별도 서비스에서 처리하도록
|
||||||
|
// 구조만 유지하고 세부 처리는 IndicatorProcessService로 위임한다.
|
||||||
|
// (이 메서드 자체는 기존 WebConnection.CalculateDataList 진입점 역할)
|
||||||
|
foreach (var item in cloneList)
|
||||||
|
{
|
||||||
|
// TODO: 필요 시 기존 시간대별 병합 로직 포팅
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WebSocket 데이터 처리 중 오류");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try { _cts?.Cancel(); } catch { }
|
||||||
|
|
||||||
|
if (_webSocket != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_webSocket.State == WebSocketState.Open)
|
||||||
|
await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "WebSocket 연결 종료", ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
_webSocket.Dispose();
|
||||||
|
_webSocket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pingLoopTask = null;
|
||||||
|
_receiveLoopTask = null;
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await StopAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
Bitget_Server/Infrastructure/Utils/BaseSettings.cs
Normal file
79
Bitget_Server/Infrastructure/Utils/BaseSettings.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Infrastructure.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 파일에 자동 영속화되는 싱글톤 설정 베이스.
|
||||||
|
/// 기존 WinForms BaseSettings<T> 로직을 유지하되, ASP.NET Core 환경에서는
|
||||||
|
/// 파일 저장 경로를 /data 볼륨(컨테이너) 또는 AppContext.BaseDirectory로 사용한다.
|
||||||
|
/// </summary>
|
||||||
|
public class BaseSettings<T> where T : class, new()
|
||||||
|
{
|
||||||
|
private static readonly object lockObject = new object();
|
||||||
|
private static readonly object fileLockObject = new object();
|
||||||
|
private static T _instance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 설정 파일을 저장할 디렉토리. Program.cs에서 시작 시 지정한다 (기본: AppContext.BaseDirectory).
|
||||||
|
/// </summary>
|
||||||
|
public static string StorageDirectory { get; set; } = AppContext.BaseDirectory;
|
||||||
|
|
||||||
|
public static T Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (lockObject)
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
_instance = CreateInstance();
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static T CreateInstance()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var saveFilePath = GetFilePath();
|
||||||
|
if (File.Exists(saveFilePath))
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<T>(File.ReadAllText(saveFilePath)) ?? new T();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 손상 시 기본값으로 복구
|
||||||
|
}
|
||||||
|
return new T();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFilePath()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(StorageDirectory);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return Path.Combine(StorageDirectory, typeof(T).Name + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveConfig()
|
||||||
|
{
|
||||||
|
lock (fileLockObject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||||
|
File.WriteAllText(GetFilePath(), json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 저장 실패는 무시 (로그는 상위 레이어에서)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>SaveConfig 의 짧은 별칭.</summary>
|
||||||
|
public void Save() => SaveConfig();
|
||||||
|
}
|
||||||
35
Bitget_Server/Infrastructure/Utils/Convertor.cs
Normal file
35
Bitget_Server/Infrastructure/Utils/Convertor.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Bitget_Server.Infrastructure.Utils;
|
||||||
|
|
||||||
|
public static class Convertor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enum의 Description Attribute 문자열을 가져온다.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetEnumDescription(Enum source)
|
||||||
|
{
|
||||||
|
FieldInfo fi = source.GetType().GetField(source.ToString());
|
||||||
|
if (fi == null) return source.ToString();
|
||||||
|
|
||||||
|
var att = (DescriptionAttribute)fi.GetCustomAttribute(typeof(DescriptionAttribute));
|
||||||
|
return att != null ? att.Description : source.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DateTime -> Unix 타임스탬프(초 단위, KST 9시 기준). 기존 WinForms 로직과 동일.
|
||||||
|
/// </summary>
|
||||||
|
public static long GetDateTimeToUnixtimeStamp(DateTime datetime)
|
||||||
|
{
|
||||||
|
return (long)(datetime - new DateTime(1970, 1, 1, 9, 0, 0)).TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unix ms 타임스탬프 -> DateTime (KST 9시 기준).
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime GetUnixtimeStampToDateTime(long unixStamptime)
|
||||||
|
{
|
||||||
|
return new DateTime(1970, 1, 1, 9, 0, 0).AddMilliseconds(unixStamptime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Bitget_Server.EFCore;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Bitget_Server.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(BitgetContext))]
|
|
||||||
[Migration("20240602102008_init1")]
|
|
||||||
partial class init1
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "8.0.6")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
|
||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Bitget_Server.EFCore.Models.PushAlarm", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("AlarmTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("Contents")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("PushAlarms");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Bitget_Server.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class init1 : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PushAlarms",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "int", nullable: false)
|
|
||||||
.Annotation("SqlServer:Identity", "1, 1"),
|
|
||||||
AlarmTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
|
||||||
Title = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
|
||||||
Contents = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PushAlarms", x => x.Id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PushAlarms");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user