49 Commits

Author SHA1 Message Date
kyh
9fce4b8636 short수정 2025-09-15 12:51:30 +09:00
kyh
738783473c 수정 2025-08-26 12:49:38 +09:00
kyh
f6b5cdda37 sell price 수정 2025-08-22 15:15:20 +09:00
kyh
424f4fad73 오류 수정 2025-08-18 10:30:27 +09:00
kyh
bb9777f2b1 retouch 코드 수정 2025-08-13 12:49:22 +09:00
kyh
d9ad08d6ad high 조건 추가 2025-08-11 09:56:02 +09:00
kyh
d030c3004c Short 관련 수정 2025-08-08 10:57:12 +09:00
kyh
73c30648d7 재수정 2025-08-06 15:06:38 +09:00
kyh
e02cb70d30 실식가 rate 처리 반영
Short 개선 반영
2025-08-06 10:51:08 +09:00
kyh
29cdaa5156 Sideway Percent로직 추가 2025-07-31 13:36:43 +09:00
kyh
e6e9753b6d 비율로 들어가게 처리 2025-07-25 13:51:05 +09:00
kyh
86aee0b03a 현재꺼로 수정 2025-07-22 12:58:11 +09:00
kyh
3a6423a071 수정 2025-07-07 10:55:01 +09:00
kyh
61ba50e118 url 필요 없어서 뻄 2025-07-03 10:23:05 +09:00
kyh
db2ca84912 판매 시점이 온후로 처리 (pine view와 같이 동작하는거로 보기 위함) 2025-06-26 11:41:34 +09:00
kyh
9f2e5943a7 30분으로 ragne %고침 2025-06-26 11:40:45 +09:00
kyh
240a53aa51 롱 올림 2025-06-26 11:40:32 +09:00
kyh
98c5eb598c chat gpt 이상해서 내가 계산 함 2025-06-19 12:16:35 +09:00
kyh
b2813cc62c 파는 로직에 추가함 2025-06-19 10:48:45 +09:00
kyh
d684edc831 푸시ㅜ푸ㅡ쉬 2025-06-18 15:35:54 +09:00
kyh
044e72664a 전저가 고가 추가해봄 2025-06-17 14:58:00 +09:00
kyh
21440503be SIDE WAY 기준점 변경(높은값 기준이 아닌 낮은값 기준으로 변경) 2025-06-17 13:50:36 +09:00
kyh
122a283cd5 초기화 로직 추가 2025-06-17 11:41:09 +09:00
kyh
00a5860b29 재 수정 2025-06-17 11:33:12 +09:00
kyh
6e05d15571 0.7ㅂ젼경 2025-06-17 11:30:02 +09:00
kyh
52b5fa8015 30분봉 5분봉으로
결과 확인 빠르게 하기 위함

대신 away 거르기 위한 0.7퍼로 변경(12개봉)
2025-06-17 11:28:53 +09:00
kyh
3fe80ee611 데이터 변경 2025-06-17 11:22:43 +09:00
kyh
abf8a4f7f2 수정 2025-06-16 15:05:53 +09:00
kyh
4742970f2d 2.2 로 변경 BTC추가 위해 2025-06-16 13:23:38 +09:00
kyh
9a60569f94 판매조건 수정 2025-06-16 13:23:15 +09:00
kyh
d9aba561cb 삿는데 다음봉 되보니 아닐경우에 대한 예외처리 추가(원본가 오면 팔아버림) 2025-06-16 13:15:49 +09:00
kyh
68548e05f4 isside 관련 오류 수정(자기자신포함하지 않음) 2025-06-11 10:50:50 +09:00
kyh
d32225c46f 구름떼 수정 2025-06-09 12:58:22 +09:00
kyh
ebe11900ee 마지ㅏㄱ 2025-06-09 11:37:10 +09:00
kyh
53ee8f1595 최종 2025-06-09 11:27:53 +09:00
kyh
c2870042f7 wotnwjhd 2025-06-04 10:53:51 +09:00
kyh
0f3e8293de 이상한거 수정 2025-06-04 10:29:49 +09:00
kyh
504cfd8af6 가격으로 사니 늦어서 못삼 현재가로 사도록 수정 2025-05-30 09:43:42 +09:00
kyh
5c797e2b11 구름떼 오류 수정 2025-05-29 13:40:56 +09:00
kyh
b698428681 Short은 구름떼 이후 첫번째만 처리 함(Short은 한번만 가는경우가 많아서) 2025-05-29 11:57:43 +09:00
kyh
d11c703670 test 2025-05-29 11:42:52 +09:00
kyh
07085f88c7 사는 지점과 파는지점이 겹치면 안사는거로 처리 2025-05-28 15:37:09 +09:00
kyh
4a39cf6e70 short수정 2025-05-28 12:51:21 +09:00
kyh
9a90554399 마지막 수정 (코드 간결화) 2025-05-27 14:20:54 +09:00
kyh
ba7b46f86d 재 수정 2025-05-27 13:13:16 +09:00
kyh
a2c307b3d8 buy, sell 정규화 2025-05-27 11:40:01 +09:00
kyh
3be354f879 리플용으로 설정값 지정 2025-05-23 13:17:18 +09:00
kyh
f70aff1a1e 데이터 수정 2025-05-23 12:44:23 +09:00
kyh
b7cf26b8fc 30분봉 제외 모든거 삭제 2025-05-23 11:03:30 +09:00
111 changed files with 2140 additions and 6241 deletions

View File

@@ -1,30 +0,0 @@
**/.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
View File

@@ -11,4 +11,3 @@
/packages /packages
/.cr /.cr
/.idea /.idea
/Bitget_Server/logs

View File

@@ -5,7 +5,9 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitget_Server", "Bitget_Server\Bitget_Server.csproj", "{22361711-30AD-4CFE-8D21-00868CA322EA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitget_Server", "Bitget_Server\Bitget_Server.csproj", "{725DC784-0243-4FDF-921E-91CF09580A03}"
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
@@ -17,10 +19,16 @@ 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
{22361711-30AD-4CFE-8D21-00868CA322EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {725DC784-0243-4FDF-921E-91CF09580A03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22361711-30AD-4CFE-8D21-00868CA322EA}.Debug|Any CPU.Build.0 = Debug|Any CPU {725DC784-0243-4FDF-921E-91CF09580A03}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22361711-30AD-4CFE-8D21-00868CA322EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {725DC784-0243-4FDF-921E-91CF09580A03}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22361711-30AD-4CFE-8D21-00868CA322EA}.Release|Any CPU.Build.0 = Release|Any CPU {725DC784-0243-4FDF-921E-91CF09580A03}.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

View File

@@ -112,6 +112,7 @@
<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" />
@@ -130,7 +131,6 @@
<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>

View File

@@ -48,6 +48,15 @@ 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;

View File

@@ -4,6 +4,7 @@ using BitgetApi.IndicatorConditionSetup;
using BitgetApi.IndicatorData; using BitgetApi.IndicatorData;
using BitgetAPI.Data; using BitgetAPI.Data;
using BitgetAPI.WebService; using BitgetAPI.WebService;
using DevExpress.XtraGrid.Views.Base;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
using Serilog.Debugging; using Serilog.Debugging;
@@ -30,7 +31,7 @@ namespace BitgetAPI.IndicatorConditionSetup
return options.SelectAPI.AccessKey; return options.SelectAPI.AccessKey;
} }
} }
// = "bg_f93aa530288b6ec9d62095994c51972a"; // = "bg_f93aa55288b6ec9d62095994c51972a";
private string YHKIM_SecreteKey private string YHKIM_SecreteKey
{ {
get get
@@ -114,14 +115,14 @@ namespace BitgetAPI.IndicatorConditionSetup
public Dictionary<string, Dictionary<int, bool>> SellShortStep = new Dictionary<string, Dictionary<int, bool>>(); public Dictionary<string, Dictionary<int, bool>> SellShortStep = new Dictionary<string, Dictionary<int, bool>>();
public Dictionary<string, Dictionary<int, bool>> SellLongStep = new Dictionary<string, Dictionary<int, bool>>(); public Dictionary<string, Dictionary<int, bool>> SellLongStep = new Dictionary<string, Dictionary<int, bool>>();
public Dictionary<string, int> BuyShortStep = new Dictionary<string, int>(); //public Dictionary<string, int> BuyShortStep = new Dictionary<string, int>();
public Dictionary<string, int> BuyLongStep = new Dictionary<string, int>(); //public Dictionary<string, int> BuyLongStep = new Dictionary<string, int>();
public Dictionary<string, double> BuyShortStepStampData = new Dictionary<string, double>(); //public Dictionary<string, double> BuyShortStepStampData = new Dictionary<string, double>();
public Dictionary<string, double> BuyLongStepStampData = new Dictionary<string, double>(); //public Dictionary<string, double> BuyLongStepStampData = new Dictionary<string, double>();
public Dictionary<string, bool> FirstFlag = new Dictionary<string, bool>();
public int 전가Flag = 1;
// 전봉이 구름떼 돌파후 시가가 구름떼 위에서 시작하는 것 // 전봉이 구름떼 돌파후 시가가 구름떼 위에서 시작하는 것
@@ -133,6 +134,7 @@ namespace BitgetAPI.IndicatorConditionSetup
IchimokuConditionSetup ichimokuConditionSetup = new IchimokuConditionSetup(); IchimokuConditionSetup ichimokuConditionSetup = new IchimokuConditionSetup();
MovingAverageConditionSetup movingAverageConditionSetup = new MovingAverageConditionSetup(); MovingAverageConditionSetup movingAverageConditionSetup = new MovingAverageConditionSetup();
TradingViewConditionSetup tradingViewConditionSetup = new TradingViewConditionSetup(); TradingViewConditionSetup tradingViewConditionSetup = new TradingViewConditionSetup();
SMABandConditionSetup sMABandConditionSetup = new SMABandConditionSetup();
signals_LU.Clear(); signals_LU.Clear();
signals_SD.Clear(); signals_SD.Clear();
signals_LD.Clear(); signals_LD.Clear();
@@ -151,7 +153,7 @@ namespace BitgetAPI.IndicatorConditionSetup
//var sellData = FutureOrder("XRPUSDT", FutureMarginMode.isolated, 10, 0, FutureSide.SellShort, OrderType.market, 10, orderDic["orderId"]); //var sellData = FutureOrder("XRPUSDT", FutureMarginMode.isolated, 10, 0, FutureSide.SellShort, OrderType.market, 10, orderDic["orderId"]);
var totalImperseMACDData = new Dictionary<string, Dictionary<DateTime, ImpulseMacdData>>(); var totalImperseMACDData = new Dictionary<string, Dictionary<DateTime, ImpulseMacdData>>();
var 전저고점Flag = true;
// 추후 공통화 및 소스정리 필요 // 추후 공통화 및 소스정리 필요
while (true) while (true)
{ {
@@ -229,467 +231,139 @@ namespace BitgetAPI.IndicatorConditionSetup
var strInterval = Utils.Convertor.GetEnumDescription((Enum)interval); var strInterval = Utils.Convertor.GetEnumDescription((Enum)interval);
if (!intervalList.Contains(strInterval)) continue; if (!intervalList.Contains(strInterval)) continue;
if (nowTime.Minute == 0 && nowTime.Hour == 24 && 전저고점Flag) //if (nowTime.Minute == 0 && nowTime.Hour == 24 && 전저고점Flag)
//{
// 전저고점.Clear();
// 전저고점Flag = false;
//}
if (!FirstFlag.ContainsKey(market + strInterval + "Long"))
{ {
전저고점.Clear(); FirstFlag.Add(market + strInterval + "Long", false);
전저고점Flag = false; FirstFlag.Add(market + strInterval + "Short", false);
//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 }
//};
//전저고점.Add(market + strInterval, dic);
//var candleDatas_Temp = findedMarket.candleDataList[interval];
//var candleDatas_정렬_Temp = candleDatas_Temp.OrderBy(i => i.Argument).ToList();
//var candleDatas_Temp_List = new List<CandleData>();
//totalImperseMACDData[market + strInterval] = new Dictionary<DateTime, ImpulseMacdData>();
//for (var i = 0; i < 199; i++)
// candleDatas_Temp_List.Add(candleDatas_정렬_Temp[i]);
//for (var i = 199; i < candleDatas_정렬_Temp.Count; i++)
//{
// var candleData_Main = candleDatas_정렬_Temp[i];
// if (candleData_Main == null) continue;
// candleDatas_Temp_List.Add(candleData_Main);
// ichimokuConditionSetup.calIchimokuData(candleDatas_Temp_List, candleData_Main);
// movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_Temp_List, candleData_Main);
// if (i > 210)
// {
// var 현재봉4_Temp = candleDatas_Temp_List[i].movingAverageDataList[4].Value;
// var 전봉4_Temp = candleDatas_Temp_List[i - 1].movingAverageDataList[4].Value;
// var 전전봉4_Temp = candleDatas_Temp_List[i - 2].movingAverageDataList[4].Value;
// var 전전전봉4_Temp = candleDatas_Temp_List[i - 3].movingAverageDataList[4].Value;
// var returnVal = Calculate전고점(market, strInterval, interval, 전봉4_Temp, 전전봉4_Temp, 전전전봉4_Temp, candleDatas_Temp_List[candleDatas_Temp_List.Count - 1]);
// }
//}
} }
//else {
// if(nowTime.Minute > 0 && nowTime.Hour == 24)
// 전저고점Flag = true;
//}
if (!전저고점.ContainsKey(market + strInterval)) //if (!BuyShortStep.ContainsKey(market))
{ //{
var dic = new Dictionary<string, double> // BuyShortStep[market] = 0;
{ // BuyLongStep[market] = 0;
{ "전고가", 0 }, //}
{ "전저가", 0 }, //if (!(전저고점.ContainsKey(market + "30m")))
{ "전전고가", 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 }
};
전저고점.Add(market + strInterval, dic);
var candleDatas_Temp = findedMarket.candleDataList[interval];
var candleDatas_정렬_Temp = candleDatas_Temp.OrderBy(i => i.Argument).ToList();
var candleDatas_Temp_List = new List<CandleData>();
totalImperseMACDData[market + strInterval] = new Dictionary<DateTime, ImpulseMacdData>();
for (var i = 0; i < 199; i++)
candleDatas_Temp_List.Add(candleDatas_정렬_Temp[i]);
for (var i = 199; i < candleDatas_정렬_Temp.Count; i++)
{
var candleData_Main = candleDatas_정렬_Temp[i];
if (candleData_Main == null) continue;
candleDatas_Temp_List.Add(candleData_Main);
ichimokuConditionSetup.calIchimokuData(candleDatas_Temp_List, candleData_Main);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_Temp_List, candleData_Main);
if (i > 210)
{
var 현재봉4_Temp = candleDatas_Temp_List[i].movingAverageDataList[4].Value;
var 전봉4_Temp = candleDatas_Temp_List[i - 1].movingAverageDataList[4].Value;
var 전전봉4_Temp = candleDatas_Temp_List[i - 2].movingAverageDataList[4].Value;
var 전전전봉4_Temp = candleDatas_Temp_List[i - 3].movingAverageDataList[4].Value;
var returnVal = Calculate전고점(market, strInterval, interval, 전봉4_Temp, 전전봉4_Temp, 전전전봉4_Temp, candleDatas_Temp_List[candleDatas_Temp_List.Count - 1]);
}
}
}
else {
if(nowTime.Minute > 0 && nowTime.Hour == 24)
전저고점Flag = true;
}
if (!BuyShortStep.ContainsKey(market))
{
BuyShortStep[market] = 0;
BuyLongStep[market] = 0;
}
if (!(전저고점.ContainsKey(market + "5m")))
continue;
//if (!(전저고점.ContainsKey(market + "1m") && 전저고점.ContainsKey(market + "5m") && 전저고점.ContainsKey(market + "30m")))
// continue; // continue;
#region 1분봉 처리
// 시간별로 정렬하기 (임시 리스트)
//var candleDatas = findedMarket.candleDataList[MCandleTimeType.M1];
//var candleDatas_정렬 = candleDatas.OrderBy(i => i.Argument).ToList();
//var calMacdCandleDatas_정렬 = candleDatas.OrderBy(i => i.Argument).ToList();
//var candleData = candleDatas.Find(i => i.Argument == candleDatas_정렬.Last().Argument);
//if (candleData == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_정렬, candleData);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_정렬, candleData);
//candleDatas_정렬.RemoveAt(candleDatas_정렬.Count - 1);
//var candleData_전봉 = candleDatas.Find(i => i.Argument == candleDatas_정렬.Last().Argument);
//if (candleData_전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_정렬, candleData_전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_정렬, candleData_전봉);
//candleDatas_정렬.RemoveAt(candleDatas_정렬.Count - 1);
//var candleData_전전봉 = candleDatas.Find(i => i.Argument == candleDatas_정렬.Last().Argument);
//if (candleData_전전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_정렬, candleData_전전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_정렬, candleData_전전봉);
//candleDatas_정렬.RemoveAt(candleDatas_정렬.Count - 1);
//var candleData_전전전봉 = candleDatas.Find(i => i.Argument == candleDatas_정렬.Last().Argument);
//if (candleData_전전전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_정렬, candleData_전전전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_정렬, candleData_전전전봉);
//candleDatas_정렬.RemoveAt(candleDatas_정렬.Count - 1);
//var candleData_전전전전봉 = candleDatas.Find(i => i.Argument == candleDatas_정렬.Last().Argument);
//if (candleData_전전전전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_정렬, candleData_전전전전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_정렬, candleData_전전전전봉);
#endregion
#region 5분봉 처리
// 시간별로 정렬하기 (임시 리스트)
var candleDatas_5 = findedMarket.candleDataList[MCandleTimeType.M5];
var candleDatas_5_정렬 = candleDatas_5.OrderBy(i => i.Argument).ToList();
var candleDatas_5_TempList = candleDatas_5.OrderBy(i => i.Argument).ToList();
var candleData_5 = candleDatas_5.Find(i => i.Argument == candleDatas_5_정렬.Last().Argument);
if (candleData_5 == null) continue;
ichimokuConditionSetup.calIchimokuData(candleDatas_5_정렬, candleData_5);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_5_정렬, candleData_5);
candleDatas_5_정렬.RemoveAt(candleDatas_5_정렬.Count - 1);
var candleData_5_전봉 = candleDatas_5.Find(i => i.Argument == candleDatas_5_정렬.Last().Argument);
if (candleData_5_전봉 == null) continue;
ichimokuConditionSetup.calIchimokuData(candleDatas_5_정렬, candleData_5_전봉);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_5_정렬, candleData_5_전봉);
candleDatas_5_정렬.RemoveAt(candleDatas_5_정렬.Count - 1);
var candleData_5_전전봉 = candleDatas_5.Find(i => i.Argument == candleDatas_5_정렬.Last().Argument);
if (candleData_5_전전봉 == null) continue;
ichimokuConditionSetup.calIchimokuData(candleDatas_5_정렬, candleData_5_전전봉);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_5_정렬, candleData_5_전전봉);
candleDatas_5_정렬.RemoveAt(candleDatas_5_정렬.Count - 1);
var candleData_5_전전전봉 = candleDatas_5.Find(i => i.Argument == candleDatas_5_정렬.Last().Argument);
if (candleData_5_전전전봉 == null) continue;
ichimokuConditionSetup.calIchimokuData(candleDatas_5_정렬, candleData_5_전전전봉);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_5_정렬, candleData_5_전전전봉);
candleDatas_5_정렬.RemoveAt(candleDatas_5_정렬.Count - 1);
var candleData_5_전전전전봉 = candleDatas_5.Find(i => i.Argument == candleDatas_5_정렬.Last().Argument);
if (candleData_5_전전전전봉 == null) continue;
ichimokuConditionSetup.calIchimokuData(candleDatas_5_정렬, candleData_5_전전전전봉);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_5_정렬, candleData_5_전전전전봉);
#endregion
#region 30분봉 처리 #region 30분봉 처리
// 시간별로 정렬하기 (임시 리스트)
//var candleDatas_30 = findedMarket.candleDataList[MCandleTimeType.M30];
//var candleDatas_30_정렬 = candleDatas_30.OrderBy(i => i.Argument).ToList();
//var candleDatas_30_TempList = candleDatas_30.OrderBy(i => i.Argument).ToList();
//var candleData_30 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
//if (candleData_30 == null) continue; var candleDatas_30 = findedMarket.candleDataList[MCandleTimeType.M30];
var candleDatas_30_정렬 = candleDatas_30.OrderBy(i => i.Argument).ToList();
var candleDatas_30_TempList = candleDatas_30.OrderBy(i => i.Argument).ToList();
var candleData_30 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
//ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30); if (candleData_30 == null) continue;
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30);
////var macdData_30 = new ImpulseMacdData(); ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30);
////var imperseMacd_keys_30 = totalImperseMACDData[market + "30m"].Keys.OrderBy(t => t).ToList(); candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1);
var candleData_30_전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
////if (imperseMacd_keys_30.LastOrDefault() < candleData_30.Argument) if (candleData_30_전봉 == null) continue;
////{ ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전봉);
//// macdData_30 = impulseMacdConditionSetup.AddDataPoint(candleData_30.High, candleData_30.Low, candleData_30.Close, true); movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전봉);
//// totalImperseMACDData[market + "30m"].Add(candleData_30.Argument, macdData_30);
////}
////else
////{
//// macdData_30 = impulseMacdConditionSetup.AddDataPoint(candleData_30.High, candleData_30.Low, candleData_30.Close, false);
//// totalImperseMACDData[market + "30m"][candleData_30.Argument] = macdData_30;
////}
////if (candleData_30.impulseMacdDataList == null) candleData_30.impulseMacdDataList = new Dictionary<int, ImpulseMacdData>(); candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1);
////if (candleData_30.impulseMacdDataList.ContainsKey(30)) candleData_30.impulseMacdDataList[30] = new ImpulseMacdData(); var candleData_30_전전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
////candleData_30.impulseMacdDataList[30] = macdData_30; if (candleData_30_전전봉 == null) continue;
////var macdData_5 = new ImpulseMacdData(); ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전전봉);
movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전전봉);
////var imperseMacd_keys_5 = totalImperseMACDData[market + "5m"].Keys.OrderBy(t => t).ToList(); candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1);
var candleData_30_전전전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
////if (imperseMacd_keys_5.LastOrDefault() < candleData_5.Argument) if (candleData_30_전전전봉 == null) continue;
////{ ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전전전봉);
//// macdData_5 = impulseMacdConditionSetup.AddDataPoint(candleData_5.High, candleData_5.Low, candleData_5.Close, true); movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전전전봉);
//// totalImperseMACDData[market + "5m"].Add(candleData_5.Argument, macdData_5);
////}
////else
////{
//// macdData_5 = impulseMacdConditionSetup.AddDataPoint(candleData_5.High, candleData_5.Low, candleData_5.Close, false);
//// totalImperseMACDData[market + "5m"][candleData_5.Argument] = macdData_5;
////}
////if (candleData_5.impulseMacdDataList == null) candleData_5.impulseMacdDataList = new Dictionary<int, ImpulseMacdData>();
////if (candleData_5.impulseMacdDataList.ContainsKey(5)) candleData_5.impulseMacdDataList[5] = new ImpulseMacdData();
////candleData_30.impulseMacdDataList[5] = macdData_5;
//candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1);
//var candleData_30_전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
//if (candleData_30_전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전봉);
//candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1);
//var candleData_30_전전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
//if (candleData_30_전전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전전봉);
//candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1);
//var candleData_30_전전전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
//if (candleData_30_전전전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전전전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전전전봉);
//candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1); candleDatas_30_정렬.RemoveAt(candleDatas_30_정렬.Count - 1);
//var candleData_30_전전전전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument); var candleData_30_전전전전봉 = candleDatas_30.Find(i => i.Argument == candleDatas_30_정렬.Last().Argument);
//if (candleData_30_전전전전봉 == null) continue; if (candleData_30_전전전전봉 == null) continue;
//ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전전전전봉); ichimokuConditionSetup.calIchimokuData(candleDatas_30_정렬, candleData_30_전전전전봉);
//movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전전전전봉); movingAverageConditionSetup.Cal_YHK_Downpublic(candleDatas_30_정렬, candleData_30_전전전전봉);
#endregion #endregion
//var 저고점 = 전저고점[market + strInterval];
//if (checked구름떼상향돌파 && candleData_전봉.Open < candleData_전봉.IchimokData.상한선 && candleData.Open > candleData.IchimokData.상한선 && candleData.Low > candleData.IchimokData.상한선
// && candleData.Close > candleData.IchimokData.상한선)
//{
// var indicatorPrice = candleData.Open;
// var indicatorRate = (candleData.Close / indicatorPrice) * 100 - 100;
// var description = string.Format("{0} 구름떼 상승", strInterval);
// var signalData = signals_LU.Find(i => i.MarketName.Equals(market) && i.Description.Equals(description));
// // 없으면 생성
// if (signalData == null)
// {
// var price = candleData.Close;
// var rate = (candleData.Close / candleData.Open) * 100 - 100;
// SignalInfoData signalInfoData = new SignalInfoData();
// signalInfoData.SignalStart = nowTime;
// signalInfoData.MarketName = market;
// signalInfoData.Description = description;
// signalInfoData.Rate = rate;
// signalInfoData.Price = price;
// signalInfoData.IndicatorPrice = indicatorPrice;
// signalInfoData.IndicatorRate = indicatorRate;
// signalInfoData.MCandleTimeType = interval;
// signals_LU.Add(signalInfoData);
// }
//}
//else if (checked구름떼상향돌파 && candleData_전봉.Open > candleData_전봉.IchimokData.하한선 && candleData.Open < candleData.IchimokData.하한선 && candleData.High < candleData.IchimokData.하한선
// && candleData.Close < candleData.IchimokData.하한선)
//{
// var indicatorPrice = candleData.Open;
// var indicatorRate = (candleData.Close / indicatorPrice) * 100 - 100;
// var description = string.Format("{0} 구름떼 하락", strInterval);
// var signalData = signals_SD.Find(i => i.MarketName.Equals(market) && i.Description.Equals(description));
// // 없으면 생성
// if (signalData == null)
// {
// var price = candleData.Close;
// var rate = (candleData.Close / candleData.Open) * 100 - 100;
// SignalInfoData signalInfoData = new SignalInfoData();
// signalInfoData.SignalStart = nowTime;
// signalInfoData.MarketName = market;
// signalInfoData.Description = description;
// signalInfoData.Rate = rate;
// signalInfoData.Price = price;
// signalInfoData.IndicatorPrice = indicatorPrice;
// signalInfoData.IndicatorRate = indicatorRate;
// signalInfoData.MCandleTimeType = interval;
// signals_SD.Add(signalInfoData);
// }
//}
if (영호조건식_상향돌파 || 영호조건식_하향돌파) if (영호조건식_상향돌파 || 영호조건식_하향돌파)
{ {
#region Main 봉 계산
/*
var 전전전봉3 = candleData_전전전봉.movingAverageDataList[3].Value;
var 전전전봉4 = candleData_전전전봉.movingAverageDataList[4].Value;
var 전전전봉8 = candleData_전전전봉.movingAverageDataList[8].Value;
var 전전전봉12 = candleData_전전전봉.movingAverageDataList[12].Value;
var 전전전봉24 = candleData_전전전봉.movingAverageDataList[24].Value;
var 전전전봉36 = candleData_전전전봉.movingAverageDataList[36].Value;
var 전전전봉72 = candleData_전전전봉.movingAverageDataList[72].Value;
var 전전전봉40 = candleData_전전전봉.movingAverageDataList[40].Value;
var 전전전전봉4 = candleData_전전전전봉.movingAverageDataList[4].Value;
var 전전봉3 = candleData_전전봉.movingAverageDataList[3].Value;
var 전전봉4 = candleData_전전봉.movingAverageDataList[4].Value;
var 전전봉8 = candleData_전전봉.movingAverageDataList[8].Value;
var 전전봉12 = candleData_전전봉.movingAverageDataList[12].Value;
var 전전봉24 = candleData_전전봉.movingAverageDataList[24].Value;
var 전전봉36 = candleData_전전봉.movingAverageDataList[36].Value;
var 전전봉72 = candleData_전전봉.movingAverageDataList[72].Value;
var 전전봉144 = candleData_전전봉.movingAverageDataList[144].Value;
var 전전봉20 = candleData_전전봉.movingAverageDataList[20].Value;
var 전전봉40 = candleData_전전봉.movingAverageDataList[40].Value;
var 전전봉120 = candleData_전전봉.movingAverageDataList[120].Value;
var 전전봉160 = candleData_전전봉.movingAverageDataList[160].Value;
//var 전전봉240 = candleData_전전봉.movingAverageDataList[240].Value;
var 전전봉80 = candleData_전전봉.movingAverageDataList[80].Value;
//var 전전봉480 = candleData_전전봉.movingAverageDataList[480].Value;
var 전봉3 = candleData_전봉.movingAverageDataList[3].Value;
var 전봉4 = candleData_전봉.movingAverageDataList[4].Value;
var 전봉8 = candleData_전봉.movingAverageDataList[8].Value;
var 전봉12 = candleData_전봉.movingAverageDataList[12].Value;
var 전봉24 = candleData_전봉.movingAverageDataList[24].Value;
var 전봉36 = candleData_전봉.movingAverageDataList[36].Value;
var 전봉48 = candleData_전봉.movingAverageDataList[48].Value;
var 전봉72 = candleData_전봉.movingAverageDataList[72].Value;
var 전봉120 = candleData_전봉.movingAverageDataList[120].Value;
var 전봉144 = candleData_전봉.movingAverageDataList[144].Value;
var 전봉20 = candleData_전봉.movingAverageDataList[20].Value;
var 전봉40 = candleData_전봉.movingAverageDataList[40].Value;
var 전봉80 = candleData_전봉.movingAverageDataList[80].Value;
var 전봉160 = candleData_전봉.movingAverageDataList[160].Value;
//var 전봉240 = candleData_전봉.movingAverageDataList[240].Value;
//var 전봉480 = candleData_전봉.movingAverageDataList[480].Value;
var 현재봉3 = candleData.movingAverageDataList[3].Value;
var 현재봉4 = candleData.movingAverageDataList[4].Value;
var 현재봉8 = candleData.movingAverageDataList[8].Value;
var 현재봉12 = candleData.movingAverageDataList[12].Value;
var 현재봉24 = candleData.movingAverageDataList[24].Value;
var 현재봉36 = candleData.movingAverageDataList[36].Value;
var 현재봉20 = candleData.movingAverageDataList[20].Value;
var 현재봉40 = candleData.movingAverageDataList[40].Value;
var 현재봉48 = candleData.movingAverageDataList[48].Value;
var 현재봉72 = candleData.movingAverageDataList[72].Value;
var 현재봉120 = candleData.movingAverageDataList[120].Value;
var 현재봉144 = candleData.movingAverageDataList[144].Value;
//var 현재봉240 = candleData.movingAverageDataList[240].Value;
var 현재봉160 = candleData.movingAverageDataList[160].Value;
var 현재봉80 = candleData.movingAverageDataList[80].Value;
var 전봉Bol4_Down = candleData_전봉.movingAverageDataList[4].BolangerValue_Down;
var 전봉Bol12_Down = candleData_전봉.movingAverageDataList[12].BolangerValue_Down;
var 전봉Bol24_Down = candleData_전봉.movingAverageDataList[24].BolangerValue_Down;
var 전봉Bol72_Down = candleData_전봉.movingAverageDataList[72].BolangerValue_Down;
var 전전봉Bol4_Down = candleData_전전봉.movingAverageDataList[4].BolangerValue_Down;
var 전전봉Bol12_Down = candleData_전전봉.movingAverageDataList[12].BolangerValue_Down;
var 전전봉Bol24_Down = candleData_전전봉.movingAverageDataList[24].BolangerValue_Down;
var 전전봉Bol72_Down = candleData_전전봉.movingAverageDataList[72].BolangerValue_Down;
var Bol4_Down = candleData.movingAverageDataList[4].BolangerValue_Down;
var Bol12_Down = candleData.movingAverageDataList[12].BolangerValue_Down;
var Bol24_Down = candleData.movingAverageDataList[24].BolangerValue_Down;
var Bol72_Down = candleData.movingAverageDataList[72].BolangerValue_Down;
var 전봉Bol4_Up = candleData_전봉.movingAverageDataList[4].BolangerValue_Up;
var 전봉Bol12_Up = candleData_전봉.movingAverageDataList[12].BolangerValue_Up;
var 전봉Bol24_Up = candleData_전봉.movingAverageDataList[24].BolangerValue_Up;
var 전봉Bol72_Up = candleData_전봉.movingAverageDataList[72].BolangerValue_Up;
var 전전봉Bol4_Up = candleData_전전봉.movingAverageDataList[4].BolangerValue_Up;
var 전전봉Bol12_Up = candleData_전전봉.movingAverageDataList[12].BolangerValue_Up;
var 전전봉Bol24_Up = candleData_전전봉.movingAverageDataList[24].BolangerValue_Up;
var 전전봉Bol72_Up = candleData_전전봉.movingAverageDataList[72].BolangerValue_Up;
var Bol4_Up = candleData.movingAverageDataList[4].BolangerValue_Up;
var Bol12_Up = candleData.movingAverageDataList[12].BolangerValue_Up;
var Bol24_Up = candleData.movingAverageDataList[24].BolangerValue_Up;
var Bol72_Up = candleData.movingAverageDataList[72].BolangerValue_Up;
*/
#endregion
var 현재봉3_5 = candleData_5.movingAverageDataList[3].Value;
var 전봉3_5 = candleData_5_전봉.movingAverageDataList[3].Value;
var 전전봉3_5 = candleData_5_전전봉.movingAverageDataList[3].Value;
var 전전전봉3_5 = candleData_5_전전전봉.movingAverageDataList[3].Value;
//var 현재봉3_30 = candleData_30.movingAverageDataList[3].Value;
//var 전봉3_30 = candleData_30_전봉.movingAverageDataList[3].Value;
//var 전전봉3_30 = candleData_30_전전봉.movingAverageDataList[3].Value;
//var 전전전봉3_30 = candleData_30_전전전봉.movingAverageDataList[3].Value;
var 현재봉4_5 = candleData_5.movingAverageDataList[4].Value;
var 전봉4_5 = candleData_5_전봉.movingAverageDataList[4].Value;
var 전전봉4_5 = candleData_5_전전봉.movingAverageDataList[4].Value;
var 전전전봉4_5 = candleData_5_전전전봉.movingAverageDataList[4].Value;
//var 현재봉4_30 = candleData_30.movingAverageDataList[4].Value;
//var 전봉4_30 = candleData_30_전봉.movingAverageDataList[4].Value;
//var 전전봉4_30 = candleData_30_전전봉.movingAverageDataList[4].Value;
//var 전전전봉4_30 = candleData_30_전전전봉.movingAverageDataList[4].Value;
//var 현재봉72_30 = candleData_30.movingAverageDataList[72].Value;
var 현재봉72_5 = candleData_5.movingAverageDataList[72].Value;
//var 저고점_1 = 전저고점[market + "1m"];
var 저고점_5 = 전저고점[market + "5m"];
//var 저고점_30 = 전저고점[market + "30m"];
//var returnVal = Calculate전고점(market, "1m", MCandleTimeType.M1, 전봉4, 전전봉4, 전전전봉4, candleData);
//if (!returnVal) continue;
var returnVal = Calculate전고점(market, "5m", MCandleTimeType.M5, 전봉4_5, 전전봉4_5, 전전전봉4_5, candleData_5);
if (!returnVal) continue;
//var returnVal = Calculate전고점(market, "30m", MCandleTimeType.M30, 전봉4_30, 전전봉4_30, 전전전봉4_30, candleData_30);
//if (!returnVal) continue;
//var 전봉macdData_30 = totalImperseMACDData[market + "30m"][imperseMacd_keys_30[imperseMacd_keys_30.Count - 2]];
//var 전전봉macdData_30 = totalImperseMACDData[market + "30m"][imperseMacd_keys_30[imperseMacd_keys_30.Count - 3]];
//var 전전전봉macdData_30 = totalImperseMACDData[market + "30m"][imperseMacd_keys_30[imperseMacd_keys_30.Count - 4]];
//var 전봉macdData_5 = totalImperseMACDData[market + "5m"][imperseMacd_keys_5[imperseMacd_keys_5.Count - 2]];
//var 전전봉macdData_5 = totalImperseMACDData[market + "5m"][imperseMacd_keys_5[imperseMacd_keys_5.Count - 3]];
//var 전전전봉macdData_5 = totalImperseMACDData[market + "5m"][imperseMacd_keys_5[imperseMacd_keys_5.Count - 4]];
//신규 데이터 Setup
tradingViewConditionSetup.SetData(candleDatas_5_TempList.Select(t => t.High).ToList(),
candleDatas_5_TempList.Select(t => t.Low).ToList(), candleDatas_5_TempList.Select(t => t.Close).ToList());
if (영호조건식_상향돌파) if (영호조건식_상향돌파)
{ {
var price = candleData_5.Close; var price = candleData_30.Close;
var rate = (candleData_5.Close / candleData_5.Open) * 100 - 100; var rate = (candleData_30.Close / candleData_30.Open) * 100 - 100;
var indicatorPrice = candleData_5.Open; var indicatorPrice = candleData_30.Open;
var indicatorRate = (candleData_5.Close / indicatorPrice) * 100 - 100; var indicatorRate = (candleData_30.Close / indicatorPrice) * 100 - 100;
var description = string.Format("{0} 72,24,4 상승", strInterval); var description = string.Format("{0} 72,24,4 상승", strInterval);
var signalData = signals_SD.Find(i => i.MarketName.Equals(market) && i.Description.Equals(description)); var signalData = signals_SD.Find(i => i.MarketName.Equals(market) && i.Description.Equals(description));
@@ -701,45 +375,21 @@ namespace BitgetAPI.IndicatorConditionSetup
//사는 시점 //사는 시점
if (!UpBuyList.ContainsKey(market)) if (!UpBuyList.ContainsKey(market))
{ {
if (!FirstFlag[market + strInterval + "Long"])
var buyFlag = false;
if (!BuyLongStep.ContainsKey(market))
BuyLongStep[market] = 0;
if (BuyLongStep[market] == 0)
{ {
if (tradingViewConditionSetup.IsLongSignal()) if (sMABandConditionSetup.Calculate(candleDatas_30_TempList, SMABandType.LongSell, true))
{ {
BuyLongStep[market] = 1; FirstFlag[market + strInterval + "Long"] = true;
//BuyLongStepStampData[market] = 저고점_30["tempLowVal"];
} }
} }
if(BuyLongStep[market] == 1 )//&& BuyLongStepStampData[market] < 저고점_30["tempLowVal"]) if (sMABandConditionSetup.Calculate(candleDatas_30_TempList, SMABandType.LongBuy, true)
&& FirstFlag[market + strInterval + "Long"]
//&& 저고점["전저가"] > 저고점["전전저가"]
)
{ {
buyFlag = ((저고점_5["Type"] == 0 && //BuyLongStep[market] = 0;
저고점_5["전전전저가"] > 저고점_5["전전저가"] && 저고점_5["전전저가"] < 저고점_5["전저가"] && candleData_5_전봉.Close < 저고점_5["전고가"] && candleData_5.Close > 저고점_5["전고가"])
|| (저고점_5["Type"] == 1 &&
저고점_5["전저가"] > 저고점_5["전전저가"] && 저고점_5["전저가"] < 전봉4_5 && candleData_5_전봉.Close < 저고점_5["전고가"] && candleData_5.Close > 저고점_5["전고가"] && 전봉4_5 < 현재봉4_5 && 전봉4_5 < 전전봉4_5)
|| (저고점_5["Type"] == 0 &&
저고점_5["전전전저가"] > 저고점_5["전전저가"] && 저고점_5["전전저가"] > 저고점_5["전저가"] && candleData_5_전봉.Close < 저고점_5["전고가"] && candleData_5.Close > 저고점_5["전고가"]))
&& 전봉4_5 < 현재봉4_5 && candleData_5.Open < candleData_5.Close;
//초기화 처리 필요
if (저고점_5["전고가"] < 저고점_5["전전고가"] ||
저고점_5["전저가"] < 저고점_5["전전저가"] ||
tradingViewConditionSetup.IsShortSignal())
{
BuyLongStep[market] = 0;
buyFlag = false;
}
}
if (buyFlag)
{
BuyLongStep[market] = 0;
// 없으면 생성 // 없으면 생성
if (signalData == null) if (signalData == null)
{ {
@@ -761,10 +411,10 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "MarketName", market }, { "MarketName", market },
{ "BuyDate", nowTime }, { "BuyDate", nowTime },
{ "Price", price }, { "Price", price },
{ "Flag", 저고점_5["전고가"] > 저고점_5["전전고가"]}, //{ "Flag", 저고점["전저가"] > 저고점["전전저가"] },
{ "MCandleTimeType", (int)interval }, { "MCandleTimeType", (int)interval },
{ "CandleTime", candleData_5.Argument }, { "CandleTime", candleData_30.Argument },
{ "Candle", candleData_5 } { "Candle", candleData_30 }
}; };
UpBuyList.Add(market, buyDic); UpBuyList.Add(market, buyDic);
@@ -781,7 +431,7 @@ namespace BitgetAPI.IndicatorConditionSetup
if (LongCount < 5) if (LongCount < 5)
{ {
double size = YHKIM_OrderAmount / price * YHKIM_Leverage; double size = YHKIM_OrderAmount / price * YHKIM_Leverage;
var orderData = FutureOrder(market, FutureMarginMode.isolated, size, price, FutureSide.BuyLong, OrderType.limit, YHKIM_Leverage); var orderData = FutureOrder(market, FutureMarginMode.isolated, size, 0, FutureSide.BuyLong, OrderType.market, YHKIM_Leverage);
if (orderData != null) if (orderData != null)
{ {
var orderDic = JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonConvert.SerializeObject(orderData["data"])); var orderDic = JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonConvert.SerializeObject(orderData["data"]));
@@ -792,7 +442,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "BuyDate", nowTime }, { "BuyDate", nowTime },
{ "Price", price }, { "Price", price },
{ "MCandleTimeType", (int)interval }, { "MCandleTimeType", (int)interval },
{ "Candle", candleData_5 }, { "Candle", candleData_30 },
{ "Size", size }, { "Size", size },
{ "orderId", orderDic["orderId"] } { "orderId", orderDic["orderId"] }
}; };
@@ -808,12 +458,13 @@ namespace BitgetAPI.IndicatorConditionSetup
{ {
var buyItem = UpBuyList[market]; var buyItem = UpBuyList[market];
var flag = (bool)buyItem["Flag"];
if (interval < MCandleTimeType.H1) if (interval < MCandleTimeType.H1)
{ {
var buyTime = ((DateTime)buyItem["BuyDate"]).AddMinutes(30); var buyTime = ((DateTime)buyItem["BuyDate"]).AddMinutes(30);
if (nowTime < buyTime) if (nowTime < buyTime)
continue; continue;
} }
//임시로 처리 //임시로 처리
else else
@@ -825,19 +476,22 @@ namespace BitgetAPI.IndicatorConditionSetup
var sellFlag = false; var sellFlag = false;
if( 저고점_5["전고가"] < 저고점_5["전전고가"] || //if (!flag)
저고점_5["전저가"] > candleData_5_전봉.Close || //{
tradingViewConditionSetup.IsShortSignal()) // if (저고점["전저가"] > 저고점["전전저가"])
{ // buyItem["Flag"] = true;
sellFlag = true; //}
} //else
//{
// if (저고점["전저가"] < 저고점["전전저가"] || 저고점["전저가"] > candleData_30_전봉.Close)
// sellFlag = true;
//}
if (sMABandConditionSetup.Calculate(candleDatas_30_TempList, SMABandType.LongSell, true) || sellFlag)
if (sellFlag)
{ {
double.TryParse(buyItem["Price"].ToString(), out var buyPrice); double.TryParse(buyItem["Price"].ToString(), out var buyPrice);
var resultRate = ((price - buyPrice) / buyPrice) * 100; var resultRate = ((price - buyPrice) / buyPrice) * 100;
BuyLongStep[market] = 0; //BuyLongStep[market] = 0;
var result = new Dictionary<string, object>() var result = new Dictionary<string, object>()
{ {
{ "MarketName", market }, { "MarketName", market },
@@ -848,7 +502,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "BuyCandle", buyItem["Candle"] }, { "BuyCandle", buyItem["Candle"] },
{ "SellDate", nowTime}, { "SellDate", nowTime},
{ "SellPrice", price }, { "SellPrice", price },
{ "SellCandle", candleData_5 }, { "SellCandle", candleData_30 },
{ "Rate", resultRate } { "Rate", resultRate }
}; };
@@ -889,7 +543,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "BuyCandle", buyItem["Candle"] }, { "BuyCandle", buyItem["Candle"] },
{ "SellDate", nowTime}, { "SellDate", nowTime},
{ "SellPrice", price}, { "SellPrice", price},
{ "SellCandle", candleData_5 }, { "SellCandle", candleData_30 },
{ "Rate", resultRateData } { "Rate", resultRateData }
}; };
ResultList.Add(orderResult); ResultList.Add(orderResult);
@@ -909,10 +563,10 @@ namespace BitgetAPI.IndicatorConditionSetup
} }
if (영호조건식_하향돌파) if (영호조건식_하향돌파)
{ {
var price = candleData_5.Close; var price = candleData_30.Close;
var rate = (candleData_5.Close / candleData_5.Open) * 100 - 100; var rate = (candleData_30.Close / candleData_30.Open) * 100 - 100;
var indicatorPrice = candleData_5.Open; var indicatorPrice = candleData_30.Open;
var indicatorRate = (candleData_5.Close / indicatorPrice) * 100 - 100; var indicatorRate = (candleData_30.Close / indicatorPrice) * 100 - 100;
var description = string.Format("{0} 72,24,4 하락", strInterval); var description = string.Format("{0} 72,24,4 하락", strInterval);
var signalData = signals_SD.Find(i => i.MarketName.Equals(market) && i.Description.Equals(description)); var signalData = signals_SD.Find(i => i.MarketName.Equals(market) && i.Description.Equals(description));
@@ -922,43 +576,20 @@ namespace BitgetAPI.IndicatorConditionSetup
//사는 시점 //사는 시점
if (!DownBuyList.ContainsKey(market)) if (!DownBuyList.ContainsKey(market))
{ {
if (!FirstFlag[market + strInterval + "Short"])
var buyFlag = false;
if (!BuyShortStep.ContainsKey(market))
BuyShortStep[market] = 0;
if (BuyShortStep[market] == 0)
{ {
if (tradingViewConditionSetup.IsShortSignal()) if (sMABandConditionSetup.Calculate(candleDatas_30_TempList, SMABandType.ShortSell, true))
{ {
BuyShortStep[market] = 1; FirstFlag[market + strInterval + "Short"] = true;
BuyShortStepStampData[market] = 저고점_5["tempHighVal"];
} }
} }
if (BuyShortStep[market] == 1 && BuyShortStepStampData[market] < 저고점_5["tempHighVal"])
{
buyFlag = ((저고점_5["Type"] == 1 &&
저고점_5["전전전고가"] < 저고점_5["전전고가"] && 저고점_5["전전고가"] > 저고점_5["전고가"] && candleData_5.Close < 저고점_5["전저가"] && candleData_5_전봉.Close > 저고점_5["전저가"])
|| (저고점_5["Type"] == 0 &&
저고점_5["전고가"] < 저고점_5["전전고가"] && 저고점_5["전고가"] > 전봉4_5 && candleData_5_전봉.Close > 저고점_5["전저가"] && candleData_5.Close < 저고점_5["전저가"] && 전봉4_5 > 현재봉4_5 && 전봉4_5 > 전전봉4_5)
|| (저고점_5["Type"] == 1 &&
저고점_5["전전전고가"] < 저고점_5["전전고가"] && 저고점_5["전전고가"] < 저고점_5["전고가"] && candleData_5_전봉.Close > 저고점_5["전저가"] && candleData_5.Close < 저고점_5["전저가"]))
&& 전봉4_5 > 현재봉4_5 && candleData_5.Open > candleData_5.Close;
}
else if (저고점_5["전고가"] > 저고점_5["전전고가"] ||
저고점_5["전저가"] > 저고점_5["전전저가"] ||
tradingViewConditionSetup.IsLongSignal())
{
BuyShortStep[market] = 0;
buyFlag = false;
}
if (buyFlag) if (sMABandConditionSetup.Calculate(candleDatas_30_TempList, SMABandType.ShortBuy, true)
&& FirstFlag[market + strInterval + "Short"])
{ {
BuyShortStep[market] = 0; //BuyShortStep[market] = 0;
// 없으면 생성 // 없으면 생성
if (signalData == null) if (signalData == null)
{ {
@@ -979,10 +610,9 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "MarketName", market }, { "MarketName", market },
{ "BuyDate", nowTime }, { "BuyDate", nowTime },
{ "Price", price }, { "Price", price },
{ "Flag", 저고점_5["전저가"] < 저고점_5["전전저가"]},
{ "MCandleTimeType", (int)interval }, { "MCandleTimeType", (int)interval },
{ "CandleTime", candleData_5.Argument }, { "CandleTime", candleData_30.Argument },
{ "Candle", candleData_5 } { "Candle", candleData_30 }
}; };
DownBuyList.Add(market, buyDic); DownBuyList.Add(market, buyDic);
@@ -999,7 +629,7 @@ namespace BitgetAPI.IndicatorConditionSetup
if (ShortCount < 5) if (ShortCount < 5)
{ {
double size = YHKIM_OrderAmount / price * YHKIM_Leverage; double size = YHKIM_OrderAmount / price * YHKIM_Leverage;
var orderData = FutureOrder(market, FutureMarginMode.isolated, size, price, FutureSide.BuyShort, OrderType.limit, YHKIM_Leverage); var orderData = FutureOrder(market, FutureMarginMode.isolated, size, 0, FutureSide.BuyShort, OrderType.market, YHKIM_Leverage);
if (orderData != null) if (orderData != null)
{ {
var orderDic = JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonConvert.SerializeObject(orderData["data"])); var orderDic = JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonConvert.SerializeObject(orderData["data"]));
@@ -1010,7 +640,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "BuyDate", nowTime }, { "BuyDate", nowTime },
{ "Price", price }, { "Price", price },
{ "MCandleTimeType", (int)interval }, { "MCandleTimeType", (int)interval },
{ "Candle", candleData_5 }, { "Candle", candleData_30 },
{ "Size", size }, { "Size", size },
{ "orderId", orderDic["orderId"] } { "orderId", orderDic["orderId"] }
}; };
@@ -1026,33 +656,41 @@ namespace BitgetAPI.IndicatorConditionSetup
else else
{ {
var buyItem = DownBuyList[market]; var buyItem = DownBuyList[market];
var flag = (bool)buyItem["Flag"];
if (interval < MCandleTimeType.H1) if (interval < MCandleTimeType.H1)
{ {
var buyTime = ((DateTime)buyItem["BuyDate"]).AddMinutes(30); var buyTime = ((DateTime)buyItem["BuyDate"]).AddMinutes(30);
if (nowTime < buyTime) if (nowTime < buyTime)
continue; continue;
} }
//임시로 처리 //임시로 처리
else else
{ {
var buyTime = ((DateTime)buyItem["BuyDate"]).AddHours((int)interval / 60); var buyTime = ((DateTime)buyItem["BuyDate"]).AddHours((int)interval / 60);
if (nowTime < buyTime) if (nowTime < buyTime)
continue; continue;
} }
var sellFlag = false; var sellFlag = false;
if (저고점_5["전저가"] > 저고점_5["전전저가"] || //if (!flag)
저고점_5["전고가"] < candleData_5_전봉.Close || //{
tradingViewConditionSetup.IsLongSignal()) // if (저고점["전고가"] < 저고점["전전고가"])
// buyItem["Flag"] = true;
//}
//else
//{
// if (저고점["전고가"] > 저고점["전전고가"] || 저고점["전고가"] < candleData_30_전봉.Close)
// sellFlag = true;
//}
if (sMABandConditionSetup.Calculate(candleDatas_30_TempList, SMABandType.ShortSell, true) || sellFlag)
{ {
sellFlag = true; //BuyShortStep[market] = 0;
}
if (sellFlag)
{
BuyShortStep[market] = 0;
double.TryParse(buyItem["Price"].ToString(), out var buyPrice); double.TryParse(buyItem["Price"].ToString(), out var buyPrice);
var resultRate = ((price - buyPrice) / buyPrice) * 100; var resultRate = ((price - buyPrice) / buyPrice) * 100;
@@ -1066,7 +704,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "BuyCandle", buyItem["Candle"] }, { "BuyCandle", buyItem["Candle"] },
{ "SellDate", nowTime}, { "SellDate", nowTime},
{ "SellPrice", price }, { "SellPrice", price },
{ "SellCandle", candleData_5 }, { "SellCandle", candleData_30 },
{ "Rate", resultRate } { "Rate", resultRate }
}; };
@@ -1108,7 +746,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{ "BuyCandle", buyItem["Candle"] }, { "BuyCandle", buyItem["Candle"] },
{ "SellDate", nowTime}, { "SellDate", nowTime},
{ "SellPrice", price }, { "SellPrice", price },
{ "SellCandle", candleData_5 }, { "SellCandle", candleData_30 },
{ "Rate", resultRateData } { "Rate", resultRateData }
}; };
ResultList.Add(orderResult); ResultList.Add(orderResult);
@@ -1251,13 +889,13 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-TIMESTAMP",timestamp} {"ACCESS-TIMESTAMP",timestamp}
}; };
return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetPendingOrder_Future, BitgetURL_PendingOrders_Future, null, MCandleTimeType.M1, "", "", nvc); return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetPendingOrder_Future, BitgetURL_PendingOrders_Future, null, MCandleTimeType.M30, "", "", nvc);
} }
public long GetTimeStamp() public long GetTimeStamp()
{ {
var result = (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetGetTimeStamp, BitgetURL_GetTimestamp, null, MCandleTimeType.M1, "", "", null); var result = (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetGetTimeStamp, BitgetURL_GetTimestamp, null, MCandleTimeType.M30, "", "", null);
if (result == null) if (result == null)
{ {
return 0; return 0;
@@ -1283,7 +921,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-TIMESTAMP",timestamp} {"ACCESS-TIMESTAMP",timestamp}
}; };
return (List<Dictionary<string, object>>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetAccountList_Future, BitgetURL_AccountList_Future, null, MCandleTimeType.M1, "", "", nvc); return (List<Dictionary<string, object>>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetAccountList_Future, BitgetURL_AccountList_Future, null, MCandleTimeType.M30, "", "", nvc);
} }
public Dictionary<string, object> CancelFutureOrder(string symbol, string orderId) public Dictionary<string, object> CancelFutureOrder(string symbol, string orderId)
@@ -1301,7 +939,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-TIMESTAMP", timestamp} {"ACCESS-TIMESTAMP", timestamp}
}; };
return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetCancelOrder_Future, BitgetURL_CancelOrder_Future, null, MCandleTimeType.M1, postStr, "", nvc); return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetCancelOrder_Future, BitgetURL_CancelOrder_Future, null, MCandleTimeType.M30, postStr, "", nvc);
} }
public Dictionary<string, object> ChangeFuturePositionMode(FuturePositionMode mode) public Dictionary<string, object> ChangeFuturePositionMode(FuturePositionMode mode)
@@ -1320,7 +958,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-TIMESTAMP", timestamp} {"ACCESS-TIMESTAMP", timestamp}
}; };
return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetChangePositionMode_Future, BitgetURL_ChangePositionMode_Future, null, MCandleTimeType.M1, postStr, "", nvc); return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetChangePositionMode_Future, BitgetURL_ChangePositionMode_Future, null, MCandleTimeType.M30, postStr, "", nvc);
} }
public Dictionary<string, object> GetFutureOrderDetail(string symbol, string orderId) public Dictionary<string, object> GetFutureOrderDetail(string symbol, string orderId)
@@ -1338,7 +976,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-TIMESTAMP",timestamp} {"ACCESS-TIMESTAMP",timestamp}
}; };
return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetOrderDetail_Future, BitgetURL_OrderDetail_Future + "?symbol=" + symbol.ToString() + "&orderId=" + orderId.ToString() + "&productType=USDT-FUTURES", null, MCandleTimeType.M1, "", "", orderDetailnvc); return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetOrderDetail_Future, BitgetURL_OrderDetail_Future + "?symbol=" + symbol.ToString() + "&orderId=" + orderId.ToString() + "&productType=USDT-FUTURES", null, MCandleTimeType.M30, "", "", orderDetailnvc);
} }
@@ -1391,7 +1029,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-PASSPHRASE", YHKIM_PassphraseKey }, {"ACCESS-PASSPHRASE", YHKIM_PassphraseKey },
{"ACCESS-TIMESTAMP", timestamp} {"ACCESS-TIMESTAMP", timestamp}
}; };
var result = (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetChangeMarginMode_Future, BitgetURL_ChangeMarginMode_Future, null, MCandleTimeType.M1, changeMarginModeStr, "", nvcMargin); var result = (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetChangeMarginMode_Future, BitgetURL_ChangeMarginMode_Future, null, MCandleTimeType.M30, changeMarginModeStr, "", nvcMargin);
if (result == null || result["msg"]?.ToString() != "success") if (result == null || result["msg"]?.ToString() != "success")
{ {
return null; return null;
@@ -1405,7 +1043,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-PASSPHRASE", YHKIM_PassphraseKey }, {"ACCESS-PASSPHRASE", YHKIM_PassphraseKey },
{"ACCESS-TIMESTAMP", timestamp} {"ACCESS-TIMESTAMP", timestamp}
}; };
result = (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetChangeLeverage_Future, BitgetURL_ChangeLeverage_Future, null, MCandleTimeType.M1, leverageStr, "", nvcLeverage); result = (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetChangeLeverage_Future, BitgetURL_ChangeLeverage_Future, null, MCandleTimeType.M30, leverageStr, "", nvcLeverage);
if (result == null || result["msg"]?.ToString() != "success") if (result == null || result["msg"]?.ToString() != "success")
{ {
return null; return null;
@@ -1430,7 +1068,7 @@ namespace BitgetAPI.IndicatorConditionSetup
{"ACCESS-PASSPHRASE", YHKIM_PassphraseKey }, {"ACCESS-PASSPHRASE", YHKIM_PassphraseKey },
{"ACCESS-TIMESTAMP", timestamp} {"ACCESS-TIMESTAMP", timestamp}
}; };
return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetOrder_Future, BitgetURL_BuySellOrder_Future, null, MCandleTimeType.M1, postStr, "", nvc); return (Dictionary<string, object>)WebConnection.SendWebServer(ExchangeWebReqType.BitgetOrder_Future, BitgetURL_BuySellOrder_Future, null, MCandleTimeType.M30, postStr, "", nvc);
} }
public string GenerateSignString(string timestamp, string method, string requestPath, string queryString, string body, string secretKey) public string GenerateSignString(string timestamp, string method, string requestPath, string queryString, string body, string secretKey)

View File

@@ -0,0 +1,304 @@
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
};
}
}
}

View File

@@ -118,128 +118,148 @@ namespace BitgetAPI.WebService
using (var wRes = (HttpWebResponse)wReq.GetResponse()) using (var wRes = (HttpWebResponse)wReq.GetResponse())
{ {
var respPostStream = wRes.GetResponseStream(); try
var readerPost = new StreamReader(respPostStream, Encoding.UTF8, true);
var resResult = readerPost.ReadToEnd();
switch (exchangeWebReqType)
{ {
#region Bitget var respPostStream = wRes.GetResponseStream();
case CommonProperty.ExchangeWebReqType.BitgetMarket_Spot: var readerPost = new StreamReader(respPostStream, Encoding.UTF8, true);
{ var resResult = readerPost.ReadToEnd();
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult); switch (exchangeWebReqType)
var marketInfo = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString()); {
var data = marketInfo.FindAll(x => x["coin"].ToString().Contains("USDT")); #region Bitget
} 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)
{ {
MarketInfo marketInfo = new MarketInfo(); var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
marketInfo.MarketName = item["symbol"].ToString(); var marketInfo = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
marketInfo.ExchangePlace = Enum.GetName(typeof(CommonProperty.ExchangePlace), commonProperty.selectedChangePlace); var data = marketInfo.FindAll(x => x["coin"].ToString().Contains("USDT"));
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;
return commonProperty.marketInfoList; case CommonProperty.ExchangeWebReqType.BitgetMarket_Future:
}
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(); var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
var marketInfos = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
candleData.Argument = Convertor.GetUnixtimeStampToDateTime(long.Parse(item[0].ToString())); commonProperty.marketInfoList = new List<MarketInfo>();
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개밖에 없음 foreach (var item in marketInfos)
// 마지막 시간 데이터는 불완전 > 웹소켓 데이터와 합체
if (item == datas.Last())
{ {
var timespan_add = new System.TimeSpan(0, 0, 0); MarketInfo marketInfo = new MarketInfo();
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);
}
switch (interval) 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)
{
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())
{ {
case CommonProperty.MCandleTimeType.M1: var timespan_add = new System.TimeSpan(0, 0, 0);
// 같은 분이면 무조건 업데이트
timespan_add = new System.TimeSpan(0, 1, 0); switch (interval)
break; {
case CommonProperty.MCandleTimeType.M3: case CommonProperty.MCandleTimeType.M1:
// 같은 분이면 무조건 업데이트 // 같은 분이면 무조건 업데이트
timespan_add = new System.TimeSpan(0, 3, 0); timespan_add = new System.TimeSpan(0, 1, 0);
break; break;
case CommonProperty.MCandleTimeType.M5: case CommonProperty.MCandleTimeType.M3:
timespan_add = new System.TimeSpan(0, 5, 0); // 같은 분이면 무조건 업데이트
break; timespan_add = new System.TimeSpan(0, 3, 0);
case CommonProperty.MCandleTimeType.M15: break;
timespan_add = new System.TimeSpan(0, 15, 0); case CommonProperty.MCandleTimeType.M5:
break; timespan_add = new System.TimeSpan(0, 5, 0);
case CommonProperty.MCandleTimeType.M30: break;
timespan_add = new System.TimeSpan(0, 30, 0); case CommonProperty.MCandleTimeType.M15:
break; timespan_add = new System.TimeSpan(0, 15, 0);
case CommonProperty.MCandleTimeType.H1: break;
timespan_add = new System.TimeSpan(1, 0, 0); case CommonProperty.MCandleTimeType.M30:
break; timespan_add = new System.TimeSpan(0, 30, 0);
case CommonProperty.MCandleTimeType.H2: break;
timespan_add = new System.TimeSpan(2, 0, 0); case CommonProperty.MCandleTimeType.H1:
break; timespan_add = new System.TimeSpan(1, 0, 0);
case CommonProperty.MCandleTimeType.H3: break;
timespan_add = new System.TimeSpan(3, 0, 0); case CommonProperty.MCandleTimeType.H2:
break; timespan_add = new System.TimeSpan(2, 0, 0);
case CommonProperty.MCandleTimeType.H4: break;
timespan_add = new System.TimeSpan(4, 0, 0); case CommonProperty.MCandleTimeType.H3:
break; timespan_add = new System.TimeSpan(3, 0, 0);
case CommonProperty.MCandleTimeType.H6: break;
timespan_add = new System.TimeSpan(6, 0, 0); case CommonProperty.MCandleTimeType.H4:
break; timespan_add = new System.TimeSpan(4, 0, 0);
case CommonProperty.MCandleTimeType.H12: break;
timespan_add = new System.TimeSpan(12, 0, 0); case CommonProperty.MCandleTimeType.H6:
break; timespan_add = new System.TimeSpan(6, 0, 0);
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);
@@ -247,60 +267,46 @@ 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;
break; case ExchangeWebReqType.BitgetAccountList_Future:
case ExchangeWebReqType.BitgetAccountList_Future: {
{ var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult);
var datalist = JsonConvert.DeserializeObject<Dictionary<string, object>>(resResult); return JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(datalist["data"]?.ToString());
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;
} }
} }
} }
@@ -1050,7 +1056,7 @@ namespace BitgetAPI.WebService
candleDataList.Add(candleData); candleDataList.Add(candleData);
// 기존 데이터 삭제 // 기존 데이터 삭제
if (candleDataList.Count > 500) if (candleDataList.Count > 1000)
{ {
// 가장 오래된 데이터 삭제 // 가장 오래된 데이터 삭제
candleDataList.Remove(j => j.Argument == candleDataList.Min(i => i.Argument)); candleDataList.Remove(j => j.Argument == candleDataList.Min(i => i.Argument));
@@ -1107,6 +1113,7 @@ 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();
@@ -1238,7 +1245,8 @@ 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;
} }
@@ -1255,7 +1263,6 @@ namespace BitgetAPI.WebService
dt = dt.AddMinutes(dt.Minute * -1); dt = dt.AddMinutes(dt.Minute * -1);
} }
// 새 데이터 등록 // 새 데이터 등록
candleDataList.Add(candleData); candleDataList.Add(candleData);

14
Bitget_App/App.xaml Normal file
View File

@@ -0,0 +1,14 @@
<?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>

12
Bitget_App/App.xaml.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace Bitget_App
{
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
}

15
Bitget_App/AppShell.xaml Normal file
View File

@@ -0,0 +1,15 @@
<?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>

View File

@@ -0,0 +1,10 @@
namespace Bitget_App
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,76 @@
<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>

View File

@@ -0,0 +1,33 @@
<?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>

36
Bitget_App/MainPage.xaml Normal file
View File

@@ -0,0 +1,36 @@
<?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 &#10;.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>

View File

@@ -0,0 +1,33 @@
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);
}
}
}

47
Bitget_App/MauiProgram.cs Normal file
View File

@@ -0,0 +1,47 @@
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);
}
}
}

View File

@@ -0,0 +1,12 @@
<?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>

View File

@@ -0,0 +1,11 @@
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
{
}
}

View File

@@ -0,0 +1,16 @@
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();
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<string name="com.google.firebase.crashlytics.mapping_file_id">none</string>
</resources>

View File

@@ -0,0 +1,10 @@
using Foundation;
namespace Bitget_App
{
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}

View File

@@ -0,0 +1,14 @@
<?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>

View File

@@ -0,0 +1,38 @@
<?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>

View File

@@ -0,0 +1,16 @@
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));
}
}
}

View File

@@ -0,0 +1,17 @@
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);
}
}
}

View File

@@ -0,0 +1,15 @@
<?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>

View File

@@ -0,0 +1,8 @@
<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>

View File

@@ -0,0 +1,25 @@
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();
}
}

View File

@@ -0,0 +1,46 @@
<?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>

View File

@@ -0,0 +1,15 @@
<?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>

View File

@@ -0,0 +1,10 @@
using Foundation;
namespace Bitget_App
{
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}

View File

@@ -0,0 +1,32 @@
<?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>

View File

@@ -0,0 +1,16 @@
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));
}
}
}

View File

@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}

View File

@@ -0,0 +1,4 @@
<?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>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,8 @@
<?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>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -0,0 +1,15 @@
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();
}

View File

@@ -0,0 +1,8 @@
<?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>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,45 @@
<?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>

View File

@@ -0,0 +1,426 @@
<?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>

View File

@@ -0,0 +1,29 @@
{
"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"
}

View File

@@ -1,51 +0,0 @@
# =============================================================================
# .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

View File

@@ -1,47 +0,0 @@
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");
}
}
}

View File

@@ -1,212 +0,0 @@
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);
}
}

View File

@@ -1,21 +0,0 @@
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"
});
}

View File

@@ -1,148 +0,0 @@
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");
}
}
}

View File

@@ -1,97 +0,0 @@
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));
}
}

View File

@@ -1,122 +0,0 @@
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);
}
}

View File

@@ -1,66 +0,0 @@
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();
}
}

View File

@@ -1,70 +0,0 @@
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);

View File

@@ -1,18 +0,0 @@
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);

View File

@@ -1,37 +0,0 @@
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);

View File

@@ -1,19 +0,0 @@
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);

View File

@@ -1,59 +0,0 @@
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 종료");
}
}

View File

@@ -1,50 +0,0 @@
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 종료");
}
}

View File

@@ -1,62 +0,0 @@
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 종료");
}
}

View File

@@ -1,91 +0,0 @@
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;
}
}

View File

@@ -1,116 +0,0 @@
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;
}
}

View File

@@ -1,105 +0,0 @@
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;
}
}

View File

@@ -1,209 +0,0 @@
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();
}
}

View File

@@ -1,38 +0,0 @@
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();
}

View File

@@ -1,28 +0,0 @@
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;
}

View File

@@ -1,285 +0,0 @@
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>;
}
}

View File

@@ -1,78 +0,0 @@
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);
}
}
}
}
}

View File

@@ -1,146 +0,0 @@
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;
}
}

View File

@@ -1,100 +0,0 @@
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();
}
}

View File

@@ -1,386 +0,0 @@
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); }
}
}
}
}

View File

@@ -1,26 +0,0 @@
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();
}

View File

@@ -2,21 +2,21 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable> <Nullable>enable</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.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PrivateAssets>all</PrivateAssets>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" /> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> </PackageReference>
<PackageReference Include="Serilog.Sinks.Map" Version="2.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
<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>

View File

@@ -1,9 +0,0 @@
<?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>

View File

@@ -0,0 +1,19 @@
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;";
}
}

View File

@@ -0,0 +1,44 @@
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;
}
}
}
}

View File

@@ -1,65 +0,0 @@
# =============================================================================
# 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"]

View File

@@ -1,46 +0,0 @@
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

View File

@@ -1,175 +0,0 @@
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
}

View File

@@ -1,21 +0,0 @@
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;
}

View File

@@ -1,8 +0,0 @@
namespace Bitget_Server.Domain.Indicators;
public class ImpulseMacdData
{
public double MD;
public double SH;
public double SB;
}

View File

@@ -1,14 +0,0 @@
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;
}

View File

@@ -1,20 +0,0 @@
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;
}

View File

@@ -1,42 +0,0 @@
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; }
}

View File

@@ -1,16 +0,0 @@
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; }
}

View File

@@ -1,14 +0,0 @@
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; }
}

View File

@@ -1,21 +0,0 @@
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; }
}

View File

@@ -1,23 +0,0 @@
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; }
}

View File

@@ -1,19 +0,0 @@
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; }
}

View File

@@ -1,29 +0,0 @@
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; }
}

View File

@@ -0,0 +1,33 @@
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);
}
}
}

View File

@@ -0,0 +1,24 @@
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;
}
}

View File

@@ -1,277 +0,0 @@
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));
}
}

View File

@@ -1,71 +0,0 @@
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();
}
}
}

View File

@@ -1,237 +0,0 @@
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);
}
}

View File

@@ -1,79 +0,0 @@
using Newtonsoft.Json;
namespace Bitget_Server.Infrastructure.Utils;
/// <summary>
/// JSON 파일에 자동 영속화되는 싱글톤 설정 베이스.
/// 기존 WinForms BaseSettings&lt;T&gt; 로직을 유지하되, 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();
}

View File

@@ -1,35 +0,0 @@
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);
}
}

View File

@@ -0,0 +1,54 @@
// <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
}
}
}

View File

@@ -0,0 +1,37 @@
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