first commit
commit
20920c6832
|
@ -0,0 +1,30 @@
|
|||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
|
@ -0,0 +1,279 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
env/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
output/
|
||||
dbvolume/
|
||||
[Pp]roperties/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
[Log]log*
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
*.pwd
|
||||
*.env
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
|
||||
#deploy
|
||||
deploy*.bat
|
||||
.github
|
||||
|
||||
docker-compose-env/
|
||||
*.env
|
||||
|
||||
.vs
|
||||
.vscode
|
|
@ -0,0 +1,207 @@
|
|||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||
using KLHZ.Trader.Core.Declisions.Models;
|
||||
|
||||
namespace KLHZ.Trader.Core.Tests
|
||||
{
|
||||
public class Tests
|
||||
{
|
||||
private static PriceChange[] GetHistory(int count, string figi)
|
||||
{
|
||||
var res = new PriceChange[count];
|
||||
if (count != 0)
|
||||
{
|
||||
var startDt = DateTime.UtcNow.AddSeconds(-count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
startDt = startDt.AddSeconds(i);
|
||||
res[i] = new PriceChange()
|
||||
{
|
||||
Figi = figi,
|
||||
Ticker = figi + "_ticker",
|
||||
Id = i,
|
||||
Time = startDt,
|
||||
Value = (decimal)(i + 0.5)
|
||||
};
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
var count = 0;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count);
|
||||
Assert.That(data.timestamps.Length == count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test2()
|
||||
{
|
||||
var count = 1;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count);
|
||||
Assert.That(data.timestamps.Length == count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
Assert.That((float)hist[i].Value, Is.EqualTo(data.prices[i]));
|
||||
Assert.That(hist[i].Time, Is.EqualTo(data.timestamps[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test3()
|
||||
{
|
||||
var count = 20;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count);
|
||||
Assert.That(data.timestamps.Length == count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
Assert.That((float)hist[i].Value, Is.EqualTo(data.prices[i]));
|
||||
Assert.That(hist[i].Time, Is.EqualTo(data.timestamps[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test4()
|
||||
{
|
||||
var count = PriceHistoryCacheUnit.ArrayMaxLength;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count);
|
||||
Assert.That(data.timestamps.Length == count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
Assert.That((float)hist[i].Value, Is.EqualTo(data.prices[i]));
|
||||
Assert.That(hist[i].Time, Is.EqualTo(data.timestamps[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test5()
|
||||
{
|
||||
var shift = 7;
|
||||
var count = PriceHistoryCacheUnit.ArrayMaxLength + shift;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count - shift);
|
||||
Assert.That(data.timestamps.Length == count - shift);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var k = i + shift;
|
||||
if (k < hist.Length)
|
||||
{
|
||||
Assert.That((float)hist[k].Value, Is.EqualTo(data.prices[i]));
|
||||
Assert.That(hist[k].Time, Is.EqualTo(data.timestamps[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test6()
|
||||
{
|
||||
var shift = 10;
|
||||
var count = PriceHistoryCacheUnit.ArrayMaxLength + shift;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count - shift);
|
||||
Assert.That(data.timestamps.Length == count - shift);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var k = i + shift;
|
||||
if (k < hist.Length)
|
||||
{
|
||||
Assert.That((float)hist[k].Value, Is.EqualTo(data.prices[i]));
|
||||
Assert.That(hist[k].Time, Is.EqualTo(data.timestamps[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test7()
|
||||
{
|
||||
var shift = 334;
|
||||
var count = PriceHistoryCacheUnit.ArrayMaxLength + shift;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count - shift);
|
||||
Assert.That(data.timestamps.Length == count - shift);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var k = i + shift;
|
||||
if (k < hist.Length)
|
||||
{
|
||||
Assert.That((float)hist[k].Value, Is.EqualTo(data.prices[i]));
|
||||
Assert.That(hist[k].Time, Is.EqualTo(data.timestamps[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void Test8()
|
||||
{
|
||||
var count = PriceHistoryCacheUnit.ArrayMaxLength;
|
||||
var figi = "figi";
|
||||
var hist = GetHistory(count, figi);
|
||||
var cacheUnit = new PriceHistoryCacheUnit("", hist);
|
||||
var data = cacheUnit.GetData();
|
||||
|
||||
Assert.That(data.prices.Length == count);
|
||||
Assert.That(data.timestamps.Length == count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
Assert.That((float)hist[i].Value, Is.EqualTo(data.prices[i]));
|
||||
Assert.That(hist[i].Time, Is.EqualTo(data.timestamps[i]));
|
||||
}
|
||||
|
||||
var newData1 = new PriceChange() { Figi = figi, Ticker = figi, Value = 100500, Time = DateTime.UtcNow };
|
||||
|
||||
cacheUnit.AddData(newData1);
|
||||
|
||||
var data2 = cacheUnit.GetData();
|
||||
Assert.IsTrue(data2.prices[data2.prices.Length - 1] == (float)newData1.Value);
|
||||
Assert.IsTrue(data2.timestamps[data2.timestamps.Length - 1] == newData1.Time);
|
||||
|
||||
var newData2 = new PriceChange() { Figi = figi, Ticker = figi, Value = 100501, Time = DateTime.UtcNow };
|
||||
|
||||
cacheUnit.AddData(newData2);
|
||||
|
||||
var data3 = cacheUnit.GetData();
|
||||
Assert.IsTrue(data3.prices[data3.prices.Length - 1] == (float)newData2.Value);
|
||||
Assert.IsTrue(data3.timestamps[data3.timestamps.Length - 1] == newData2.Time);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||
using KLHZ.Trader.Core.Declisions.Models;
|
||||
using KLHZ.Trader.Core.Declisions.Utils;
|
||||
using Tinkoff.InvestApi.V1;
|
||||
|
||||
namespace KLHZ.Trader.Core.Tests
|
||||
{
|
||||
public class HistoryProcessingInstrumentsTests
|
||||
{
|
||||
private static PriceChange[] GetHistory(int count, string figi, DateTime startDt, float startValue, float step)
|
||||
{
|
||||
var res = new PriceChange[count];
|
||||
if (count != 0)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
startValue += step;
|
||||
startDt = startDt.AddSeconds(1);
|
||||
res[i] = new PriceChange()
|
||||
{
|
||||
Figi = figi,
|
||||
Ticker = figi + "_ticker",
|
||||
Id = i,
|
||||
Time = startDt,
|
||||
Value = (decimal)startValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test0()
|
||||
{
|
||||
var figi = "figi";
|
||||
var startDate = new DateTime(2020, 1, 1, 1, 0, 0, DateTimeKind.Utc);
|
||||
var count = 100;
|
||||
var step = 0;
|
||||
var startValue = 10;
|
||||
var unit = new PriceHistoryCacheUnit(figi, GetHistory(count, figi, startDate, startValue, step));
|
||||
|
||||
var data = unit.GetData();
|
||||
var endDate = startDate.AddSeconds(count);
|
||||
Assert.IsTrue(data.timestamps.Last() == endDate);
|
||||
Assert.IsTrue(data.prices.Last() == startValue + step * count);
|
||||
|
||||
var periodLength = 4;
|
||||
var shift = 0;
|
||||
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
|
||||
|
||||
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
|
||||
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
|
||||
var firstPrice = startValue + (step * count) - (step * (shift + periodLength));
|
||||
var lastPrice = startValue + (step * count) - step * shift;
|
||||
|
||||
var diff = firstPrice - lastPrice;
|
||||
|
||||
|
||||
Assert.IsTrue(result.LastPrice == lastPrice);
|
||||
Assert.IsTrue(result.FirstPrice == firstPrice);
|
||||
Assert.IsTrue(result.PeriodMax == maxValue);
|
||||
Assert.IsTrue(result.PeriodMin == minValue);
|
||||
Assert.IsTrue(result.PeriodDiff == diff);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
var figi = "figi";
|
||||
var startDate = new DateTime(2020, 1, 1, 1, 0, 0, DateTimeKind.Utc);
|
||||
var count = 100;
|
||||
var step = 0.5f;
|
||||
var startValue = 10;
|
||||
var unit = new PriceHistoryCacheUnit(figi, GetHistory(count, figi, startDate, startValue, step));
|
||||
|
||||
var data = unit.GetData();
|
||||
var endDate = startDate.AddSeconds(count);
|
||||
Assert.IsTrue(data.timestamps.Last() == endDate);
|
||||
Assert.IsTrue(data.prices.Last() == startValue + step * count);
|
||||
|
||||
var periodLength = 4;
|
||||
var shift = 0;
|
||||
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
|
||||
|
||||
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
|
||||
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
|
||||
var firstPrice = startValue + (step * count) - (step * (shift + periodLength));
|
||||
var lastPrice = startValue + (step * count) - step * shift;
|
||||
|
||||
var diff = lastPrice - firstPrice;
|
||||
|
||||
|
||||
Assert.IsTrue(result.LastPrice == lastPrice);
|
||||
Assert.IsTrue(result.FirstPrice == firstPrice);
|
||||
Assert.IsTrue(result.PeriodMax == maxValue);
|
||||
Assert.IsTrue(result.PeriodMin == minValue);
|
||||
Assert.IsTrue(result.PeriodDiff == diff);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test2()
|
||||
{
|
||||
var figi = "figi";
|
||||
var startDate = new DateTime(2020, 1, 1, 1, 0, 0, DateTimeKind.Utc);
|
||||
var count = 100;
|
||||
var step = 0.5f;
|
||||
var startValue = 10;
|
||||
|
||||
var unit = new PriceHistoryCacheUnit(figi, GetHistory(count, figi, startDate, startValue, step));
|
||||
|
||||
var data = unit.GetData();
|
||||
var endDate = startDate.AddSeconds(count);
|
||||
Assert.IsTrue(data.timestamps.Last() == endDate);
|
||||
Assert.IsTrue(data.prices.Last() == startValue + step * count);
|
||||
|
||||
var periodLength = 4;
|
||||
var shift = 1;
|
||||
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
|
||||
|
||||
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
|
||||
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
|
||||
var firstPrice = startValue + (step * count) - (step * (shift + periodLength));
|
||||
var lastPrice = startValue + (step * count) - step * shift;
|
||||
|
||||
var diff = lastPrice - firstPrice;
|
||||
|
||||
|
||||
Assert.IsTrue(result.LastPrice == lastPrice);
|
||||
Assert.IsTrue(result.FirstPrice == firstPrice);
|
||||
Assert.IsTrue(result.PeriodMax == maxValue);
|
||||
Assert.IsTrue(result.PeriodMin == minValue);
|
||||
Assert.IsTrue(result.PeriodDiff == diff);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test3()
|
||||
{
|
||||
var figi = "figi";
|
||||
var startDate = new DateTime(2020, 1, 1, 1, 0, 0, DateTimeKind.Utc);
|
||||
var count = 100;
|
||||
var step = -0.5f;
|
||||
var startValue = 10;
|
||||
|
||||
var unit = new PriceHistoryCacheUnit(figi, GetHistory(count, figi, startDate, startValue, step));
|
||||
|
||||
var data = unit.GetData();
|
||||
var endDate = startDate.AddSeconds(count);
|
||||
Assert.IsTrue(data.timestamps.Last() == endDate);
|
||||
Assert.IsTrue(data.prices.Last() == startValue + step * count);
|
||||
|
||||
var periodLength = 4;
|
||||
var shift = 0;
|
||||
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
|
||||
|
||||
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
|
||||
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
|
||||
var firstPrice = startValue + (step * count) - (step * (shift + periodLength));
|
||||
var lastPrice = startValue + (step * count) - step * shift;
|
||||
|
||||
var diff = lastPrice - firstPrice;
|
||||
|
||||
|
||||
Assert.IsTrue(result.LastPrice == lastPrice);
|
||||
Assert.IsTrue(result.FirstPrice == firstPrice);
|
||||
Assert.IsTrue(result.PeriodMax == maxValue);
|
||||
Assert.IsTrue(result.PeriodMin == minValue);
|
||||
Assert.IsTrue(result.PeriodDiff == diff);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test4()
|
||||
{
|
||||
var figi = "figi";
|
||||
var startDate = new DateTime(2020, 1, 1, 1, 0, 0, DateTimeKind.Utc);
|
||||
var count = 100;
|
||||
var step = -0.5f;
|
||||
var startValue = 10;
|
||||
|
||||
var unit = new PriceHistoryCacheUnit(figi, GetHistory(count, figi, startDate, startValue, step));
|
||||
|
||||
var data = unit.GetData();
|
||||
var endDate = startDate.AddSeconds(count);
|
||||
Assert.IsTrue(data.timestamps.Last() == endDate);
|
||||
Assert.IsTrue(data.prices.Last() == startValue + step * count);
|
||||
|
||||
var periodLength = 4;
|
||||
var shift = 3;
|
||||
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
|
||||
|
||||
var maxValue = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
|
||||
var minValue = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
|
||||
var firstPrice = startValue + (step * count) - (step * (shift + periodLength));
|
||||
var lastPrice = startValue + (step * count) - step * shift;
|
||||
|
||||
var diff = lastPrice - firstPrice;
|
||||
|
||||
|
||||
Assert.IsTrue(result.LastPrice == lastPrice);
|
||||
Assert.IsTrue(result.FirstPrice == firstPrice);
|
||||
Assert.IsTrue(result.PeriodMax == maxValue);
|
||||
Assert.IsTrue(result.PeriodMin == minValue);
|
||||
Assert.IsTrue(result.PeriodDiff == diff);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test5()
|
||||
{
|
||||
var figi = "figi";
|
||||
var startDate = new DateTime(2020, 1, 1, 1, 0, 0, DateTimeKind.Utc);
|
||||
var count = 100;
|
||||
var step = -0.5f;
|
||||
var startValue = 10;
|
||||
|
||||
var unit = new PriceHistoryCacheUnit(figi, GetHistory(count, figi, startDate, startValue, step));
|
||||
|
||||
var data = unit.GetData();
|
||||
var endDate = startDate.AddSeconds(count);
|
||||
Assert.IsTrue(data.timestamps.Last() == endDate);
|
||||
Assert.IsTrue(data.prices.Last() == startValue + step * count);
|
||||
|
||||
var periodLength = 4;
|
||||
var shift = 3;
|
||||
var result = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
|
||||
|
||||
var maxValue1 = startValue + (step > 0 ? (step * count) - step * shift : (step * count) - (step * (shift + periodLength)));
|
||||
var minValue1 = startValue + (step > 0 ? (step * count) - (step * (shift + periodLength)) : (step * count) - step * shift);
|
||||
var firstPrice1 = startValue + (step * count) - (step * (shift + periodLength));
|
||||
var lastPrice1 = startValue + (step * count) - step * shift;
|
||||
|
||||
var diff1 = lastPrice1 - firstPrice1;
|
||||
|
||||
|
||||
Assert.IsTrue(result.LastPrice == lastPrice1);
|
||||
Assert.IsTrue(result.FirstPrice == firstPrice1);
|
||||
Assert.IsTrue(result.PeriodMax == maxValue1);
|
||||
Assert.IsTrue(result.PeriodMin == minValue1);
|
||||
Assert.IsTrue(result.PeriodDiff == diff1);
|
||||
|
||||
|
||||
|
||||
var unit2 = new PriceHistoryCacheUnit(figi);
|
||||
var data2 = unit.GetData();
|
||||
for (int i = 0; i < data2.prices.Length; i++)
|
||||
{
|
||||
var value = (decimal)data2.prices[i];
|
||||
if (i == data2.prices.Length - 5)
|
||||
{
|
||||
value = 100;
|
||||
}
|
||||
else if (i == data2.prices.Length - 6)
|
||||
{
|
||||
value = -100;
|
||||
}
|
||||
unit2.AddData(new PriceChange()
|
||||
{
|
||||
Figi = figi,
|
||||
Ticker = figi,
|
||||
Time = data2.timestamps[i],
|
||||
Value = value
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var result2 = HistoryProcessingInstruments.GetPriceDiffForTimeSpan(unit2, TimeSpan.FromSeconds(shift), TimeSpan.FromSeconds(periodLength));
|
||||
|
||||
var maxValue2 = 100;
|
||||
var minValue2 = -100;
|
||||
|
||||
Assert.IsTrue(result2.LastPrice == result.LastPrice);
|
||||
Assert.IsTrue(result2.FirstPrice == result.FirstPrice);
|
||||
Assert.IsTrue(result2.PeriodMax == maxValue2);
|
||||
Assert.IsTrue(result2.PeriodMin == minValue2);
|
||||
Assert.IsTrue(result2.PeriodDiff == result.PeriodDiff);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KLHZ.Trader.Core\KLHZ.Trader.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="NUnit.Framework" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
//Тесты
|
||||
[assembly: InternalsVisibleTo("KLHZ.Trader.Core.Tests")]
|
|
@ -0,0 +1,45 @@
|
|||
namespace KLHZ.Trader.Core.Common
|
||||
{
|
||||
public class BotModeSwitcher
|
||||
{
|
||||
private readonly object _locker = new();
|
||||
private bool _canSell = true;
|
||||
private bool _canPurchase = true;
|
||||
|
||||
public bool CanSell()
|
||||
{
|
||||
lock (_locker)
|
||||
return _canSell;
|
||||
}
|
||||
|
||||
public bool CanPurchase()
|
||||
{
|
||||
lock (_locker)
|
||||
return _canPurchase;
|
||||
}
|
||||
|
||||
public void StopSelling()
|
||||
{
|
||||
lock (_locker)
|
||||
_canSell = false;
|
||||
}
|
||||
|
||||
public void StopPurchase()
|
||||
{
|
||||
lock (_locker)
|
||||
_canPurchase = false;
|
||||
}
|
||||
|
||||
public void StartSelling()
|
||||
{
|
||||
lock (_locker)
|
||||
_canSell = true;
|
||||
}
|
||||
|
||||
public void StartPurchase()
|
||||
{
|
||||
lock (_locker)
|
||||
_canPurchase = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
namespace KLHZ.Trader.Core.Common
|
||||
{
|
||||
internal static class Constants
|
||||
{
|
||||
public const string RubFigi = "RUB000UTSTOM";
|
||||
|
||||
public static class BotCommandsButtons
|
||||
{
|
||||
public const string DisableTrading = "Стоп торги";
|
||||
public const string EnableTrading = "Старт торги";
|
||||
|
||||
public const string DisableSelling = "Выключить продажи";
|
||||
public const string EnableSelling = "Включить продажи";
|
||||
|
||||
public const string DisablePurchases = "Выключить покупки";
|
||||
public const string EnablePurchases = "Включить покупки";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace KLHZ.Trader.Core.Common.Messaging.Contracts
|
||||
{
|
||||
public interface IDataBus
|
||||
{
|
||||
public bool AddChannel(string key, Channel<INewPriceMessage> channel);
|
||||
|
||||
public bool AddChannel(string key, Channel<TradeCommand> channel);
|
||||
public bool AddChannel(string key, Channel<INewCandle> channel);
|
||||
|
||||
public bool AddChannel(Channel<MessageForAdmin> channel);
|
||||
|
||||
public Task BroadcastNewPrice(INewPriceMessage newPriceMessage);
|
||||
|
||||
public Task BroadcastCommand(TradeCommand command);
|
||||
|
||||
public Task BroadcastCommand(MessageForAdmin command);
|
||||
public Task BroadcastNewCandle(INewCandle command);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
namespace KLHZ.Trader.Core.Common.Messaging.Contracts.Messages.Enums
|
||||
{
|
||||
public enum TradeCommandType
|
||||
{
|
||||
Unknown = 0,
|
||||
MarketBuy = 1,
|
||||
|
||||
|
||||
MarketSell = 101,
|
||||
SoftClosePosition = 110,
|
||||
|
||||
ForceClosePosition = 201,
|
||||
|
||||
UpdatePortfolio = 10000,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace KLHZ.Trader.Core.Common.Messaging.Contracts.Messages
|
||||
{
|
||||
public interface INewCandle
|
||||
{
|
||||
public bool IsHistoricalData { get; set; }
|
||||
public decimal Open { get; set; }
|
||||
public decimal Close { get; set; }
|
||||
public decimal High { get; set; }
|
||||
public decimal Low { get; set; }
|
||||
public decimal Volume { get; set; }
|
||||
public string Figi { get; set; }
|
||||
public string Ticker { get; set; }
|
||||
public DateTime Time { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
namespace KLHZ.Trader.Core.Common.Messaging.Contracts.Messages
|
||||
{
|
||||
public interface INewPriceMessage
|
||||
{
|
||||
public bool IsHistoricalData { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public string Figi { get; set; }
|
||||
public string Ticker { get; set; }
|
||||
public DateTime Time { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace KLHZ.Trader.Core.Common.Messaging.Contracts.Messages
|
||||
{
|
||||
public class MessageForAdmin
|
||||
{
|
||||
public required string Text { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
namespace KLHZ.Trader.Core.Common.Messaging.Contracts.Messages
|
||||
{
|
||||
public class NewPriceMessage : INewPriceMessage
|
||||
{
|
||||
public decimal Value { get; set; }
|
||||
public required string Figi { get; set; }
|
||||
public required string Ticker { get; set; }
|
||||
public DateTime Time { get; set; }
|
||||
public bool IsHistoricalData { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages.Enums;
|
||||
|
||||
namespace KLHZ.Trader.Core.Common.Messaging.Contracts.Messages
|
||||
{
|
||||
public class TradeCommand
|
||||
{
|
||||
public TradeCommandType CommandType { get; init; }
|
||||
public string? Figi { get; init; }
|
||||
public string? Ticker { get; init; }
|
||||
public decimal? RecomendPrice { get; init; }
|
||||
public decimal? Count { get; init; }
|
||||
public decimal? LotsCount { get; init; }
|
||||
public string? AccountId { get; init; }
|
||||
public bool IsNeedBigCashOnAccount { get; init; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace KLHZ.Trader.Core.Common.Messaging.Services
|
||||
{
|
||||
public class DataBus : IDataBus
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Channel<INewCandle>> _candlesChannels = new();
|
||||
private readonly ConcurrentDictionary<string, Channel<INewPriceMessage>> _priceChannels = new();
|
||||
private readonly ConcurrentDictionary<string, Channel<TradeCommand>> _commandChannels = new();
|
||||
private readonly ConcurrentDictionary<string, Channel<MessageForAdmin>> _chatMessages = new();
|
||||
|
||||
public bool AddChannel(Channel<MessageForAdmin> channel)
|
||||
{
|
||||
return _chatMessages.TryAdd(Guid.NewGuid().ToString(), channel);
|
||||
}
|
||||
|
||||
public bool AddChannel(string key, Channel<INewPriceMessage> channel)
|
||||
{
|
||||
return _priceChannels.TryAdd(key, channel);
|
||||
}
|
||||
|
||||
public bool AddChannel(string key, Channel<INewCandle> channel)
|
||||
{
|
||||
return _candlesChannels.TryAdd(key, channel);
|
||||
}
|
||||
|
||||
public bool AddChannel(string key, Channel<TradeCommand> channel)
|
||||
{
|
||||
return _commandChannels.TryAdd(key, channel);
|
||||
}
|
||||
|
||||
public async Task BroadcastNewPrice(INewPriceMessage newPriceMessage)
|
||||
{
|
||||
foreach (var channel in _priceChannels.Values)
|
||||
{
|
||||
await channel.Writer.WriteAsync(newPriceMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task BroadcastNewCandle(INewCandle newPriceMessage)
|
||||
{
|
||||
foreach (var channel in _candlesChannels.Values)
|
||||
{
|
||||
await channel.Writer.WriteAsync(newPriceMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task BroadcastCommand(TradeCommand command)
|
||||
{
|
||||
foreach (var channel in _commandChannels.Values)
|
||||
{
|
||||
await channel.Writer.WriteAsync(command);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task BroadcastCommand(MessageForAdmin message)
|
||||
{
|
||||
foreach (var channel in _chatMessages.Values)
|
||||
{
|
||||
await channel.Writer.WriteAsync(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace KLHZ.Trader.Core.DataLayer.Entities.Declisions
|
||||
{
|
||||
[Table("declisions")]
|
||||
public class Declision
|
||||
{
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("time")]
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
[Column("account_id")]
|
||||
public required string AccountId { get; set; }
|
||||
|
||||
[Column("figi")]
|
||||
public required string Figi { get; set; }
|
||||
|
||||
[Column("ticker")]
|
||||
public required string Ticker { get; set; }
|
||||
|
||||
[Column("price")]
|
||||
public decimal Price { get; set; }
|
||||
[Column("action")]
|
||||
public DeclisionTradeAction Action { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
namespace KLHZ.Trader.Core.DataLayer.Entities.Declisions
|
||||
{
|
||||
public enum DeclisionTradeAction
|
||||
{
|
||||
Unknown = 0,
|
||||
OpenLong = 100,
|
||||
CloseLong = 200,
|
||||
OpenShort = 300,
|
||||
CloseShort = 400,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices
|
||||
{
|
||||
[Table("candles")]
|
||||
public class Candle : INewCandle
|
||||
{
|
||||
[Column("time")]
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
[Column("figi")]
|
||||
public required string Figi { get; set; }
|
||||
|
||||
[Column("open")]
|
||||
public decimal Open { get; set; }
|
||||
[Column("close")]
|
||||
public decimal Close { get; set; }
|
||||
[Column("volume")]
|
||||
public decimal Volume { get; set; }
|
||||
|
||||
[Column("high")]
|
||||
public decimal High { get; set; }
|
||||
[Column("low")]
|
||||
public decimal Low { get; set; }
|
||||
|
||||
[Column("ticker")]
|
||||
public required string Ticker { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public bool IsHistoricalData { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace KLHZ.Trader.Core.DataLayer.Entities.Prices
|
||||
{
|
||||
[Table("price_changes")]
|
||||
public class PriceChange : INewPriceMessage
|
||||
{
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("time")]
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
[Column("value")]
|
||||
public decimal Value { get; set; }
|
||||
|
||||
[Column("figi")]
|
||||
public required string Figi { get; set; }
|
||||
|
||||
[Column("ticker")]
|
||||
public required string Ticker { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public bool IsHistoricalData { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
|
||||
{
|
||||
public enum AssetType
|
||||
{
|
||||
Unknown = 0,
|
||||
Common = 1,
|
||||
Future = 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
|
||||
{
|
||||
public enum PositionType
|
||||
{
|
||||
Unknown = 0,
|
||||
Long = 1,
|
||||
Short = 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
|
||||
{
|
||||
[Table("trades")]
|
||||
public class Trade
|
||||
{
|
||||
[Column("trade_id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("bought_at")]
|
||||
public DateTime BoughtAt { get; set; }
|
||||
|
||||
[Column("account_id")]
|
||||
public required string AccountId { get; set; }
|
||||
|
||||
[Column("figi")]
|
||||
public required string Figi { get; set; }
|
||||
|
||||
[Column("ticker")]
|
||||
public required string Ticker { get; set; }
|
||||
|
||||
[Column("price")]
|
||||
|
||||
public decimal Price { get; set; }
|
||||
|
||||
[Column("count")]
|
||||
public decimal Count { get; set; }
|
||||
|
||||
[Column("count_lots")]
|
||||
public decimal CountLots { get; set; }
|
||||
|
||||
[Column("archive_status")]
|
||||
public int ArchiveStatus { get; set; }
|
||||
|
||||
[Column("direction")]
|
||||
public TradeDirection Direction { get; set; }
|
||||
|
||||
[Column("position_type")]
|
||||
public PositionType Position { get; set; }
|
||||
|
||||
[Column("asset_type")]
|
||||
public AssetType Asset { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace KLHZ.Trader.Core.DataLayer.Entities.Trades
|
||||
{
|
||||
public enum TradeDirection
|
||||
{
|
||||
Unknown = 0,
|
||||
Income = 1,
|
||||
Outcome = 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||
using KLHZ.Trader.Core.DataLayer.Entities.Trades;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
||||
namespace KLHZ.Trader.Core.DataLayer
|
||||
{
|
||||
public class TraderDbContext : DbContext
|
||||
{
|
||||
public DbSet<Trade> Trades { get; set; }
|
||||
public DbSet<Declision> Declisions { get; set; }
|
||||
public DbSet<PriceChange> PriceChanges { get; set; }
|
||||
public DbSet<Candle> Candles { get; set; }
|
||||
public TraderDbContext(DbContextOptions<TraderDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.UseSerialColumns();
|
||||
|
||||
modelBuilder.Entity<Trade>(entity =>
|
||||
{
|
||||
entity.HasKey(e1 => e1.Id);
|
||||
entity.Property(e => e.BoughtAt)
|
||||
.HasConversion(
|
||||
v => v.ToUniversalTime(),
|
||||
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Declision>(entity =>
|
||||
{
|
||||
entity.HasKey(e1 => e1.Id);
|
||||
entity.Property(e => e.Time)
|
||||
.HasConversion(
|
||||
v => v.ToUniversalTime(),
|
||||
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PriceChange>(entity =>
|
||||
{
|
||||
entity.HasKey(e1 => e1.Id);
|
||||
entity.Ignore(e1 => e1.IsHistoricalData);
|
||||
entity.Property(e => e.Time)
|
||||
.HasConversion(
|
||||
v => v.ToUniversalTime(),
|
||||
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Candle>(entity =>
|
||||
{
|
||||
entity.HasKey(e1 => new { e1.Figi, e1.Time });
|
||||
entity.Ignore(e1 => e1.IsHistoricalData);
|
||||
entity.Property(e => e.Time)
|
||||
.HasConversion(
|
||||
v => v.ToUniversalTime(),
|
||||
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
namespace KLHZ.Trader.Core.Declisions.Models
|
||||
{
|
||||
public readonly struct PeriodPricesInfo
|
||||
{
|
||||
public readonly int Count;
|
||||
public readonly float LastPrice;
|
||||
public readonly float FirstPrice;
|
||||
public readonly float PeriodDiff;
|
||||
public readonly float PeriodMax;
|
||||
public readonly float PeriodMin;
|
||||
public readonly bool Success;
|
||||
public readonly TimeSpan Period;
|
||||
|
||||
public PeriodPricesInfo(bool success, float firstPrice, float lastPrice, float periodDiff, float periodMin, float periodMax, TimeSpan period, int count)
|
||||
{
|
||||
Success = success;
|
||||
LastPrice = lastPrice;
|
||||
FirstPrice = firstPrice;
|
||||
PeriodDiff = periodDiff;
|
||||
PeriodMax = periodMax;
|
||||
PeriodMin = periodMin;
|
||||
Period = period;
|
||||
Count = count;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
|
||||
namespace KLHZ.Trader.Core.Declisions.Models
|
||||
{
|
||||
public class PriceHistoryCacheUnit
|
||||
{
|
||||
public const int ArrayMaxLength = 500;
|
||||
|
||||
public readonly string Figi;
|
||||
|
||||
private readonly object _locker = new();
|
||||
private readonly float[] Prices = new float[ArrayMaxLength];
|
||||
private readonly DateTime[] Timestamps = new DateTime[ArrayMaxLength];
|
||||
|
||||
private int Length = 0;
|
||||
|
||||
public void AddData(INewPriceMessage priceChange)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
Array.Copy(Prices, 1, Prices, 0, Prices.Length - 1);
|
||||
Array.Copy(Timestamps, 1, Timestamps, 0, Timestamps.Length - 1);
|
||||
|
||||
Prices[Prices.Length - 1] = (float)priceChange.Value;
|
||||
Timestamps[Timestamps.Length - 1] = priceChange.Time;
|
||||
|
||||
if (Length < ArrayMaxLength)
|
||||
{
|
||||
Length++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public (DateTime[] timestamps, float[] prices) GetData()
|
||||
{
|
||||
var prices = new float[Length];
|
||||
var timestamps = new DateTime[Length];
|
||||
lock (_locker)
|
||||
{
|
||||
Array.Copy(Prices, Prices.Length - Length, prices, 0, prices.Length);
|
||||
Array.Copy(Timestamps, Prices.Length - Length, timestamps, 0, timestamps.Length);
|
||||
return (timestamps, prices);
|
||||
}
|
||||
}
|
||||
|
||||
public PriceHistoryCacheUnit(string figi, params INewPriceMessage[] priceChanges)
|
||||
{
|
||||
Figi = figi;
|
||||
|
||||
|
||||
if (priceChanges.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedPriceChanges = priceChanges
|
||||
.OrderBy(pc => pc.Time)
|
||||
.Skip(priceChanges.Length - ArrayMaxLength)
|
||||
.ToArray();
|
||||
var prices = selectedPriceChanges
|
||||
.Select(pc => (float)pc.Value)
|
||||
.ToArray();
|
||||
var times = selectedPriceChanges
|
||||
.Select(pc => pc.Time)
|
||||
.ToArray();
|
||||
|
||||
Array.Copy(prices, 0, Prices, Prices.Length - prices.Length, prices.Length);
|
||||
Array.Copy(times, 0, Timestamps, Timestamps.Length - times.Length, times.Length);
|
||||
|
||||
Length = times.Length > ArrayMaxLength ? ArrayMaxLength : times.Length;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
using KLHZ.Trader.Core.Common;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages.Enums;
|
||||
using KLHZ.Trader.Core.DataLayer;
|
||||
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
||||
using KLHZ.Trader.Core.Declisions.Models;
|
||||
using KLHZ.Trader.Core.Declisions.Utils;
|
||||
using KLHZ.Trader.Core.Exchange;
|
||||
using KLHZ.Trader.Core.Exchange.Extentions;
|
||||
using KLHZ.Trader.Core.Exchange.Models;
|
||||
using KLHZ.Trader.Core.Exchange.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
using Tinkoff.InvestApi;
|
||||
using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetType;
|
||||
|
||||
namespace KLHZ.Trader.Core.Declisions.Services
|
||||
{
|
||||
public class Trader : IHostedService
|
||||
{
|
||||
private readonly InvestApiClient _investApiClient;
|
||||
private readonly IServiceProvider _provider;
|
||||
private readonly IDataBus _dataBus;
|
||||
private readonly BotModeSwitcher _botModeSwitcher;
|
||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||
private readonly ConcurrentDictionary<string, ManagedAccount> Accounts = new();
|
||||
private readonly ConcurrentDictionary<string, PriceHistoryCacheUnit> _historyCash = new();
|
||||
|
||||
private readonly decimal _futureComission;
|
||||
private readonly decimal _shareComission;
|
||||
private readonly decimal _accountCashPart;
|
||||
private readonly decimal _accountCashPartFutures;
|
||||
private readonly decimal _defaultBuyPartOfAccount;
|
||||
private readonly string[] _managedAccountsNamePatterns = [];
|
||||
|
||||
private readonly Channel<INewPriceMessage> _pricesChannel = Channel.CreateUnbounded<INewPriceMessage>();
|
||||
|
||||
public Trader(
|
||||
BotModeSwitcher botModeSwitcher,
|
||||
IServiceProvider provider,
|
||||
IOptions<ExchangeConfig> options,
|
||||
IDataBus dataBus,
|
||||
IDbContextFactory<TraderDbContext> dbContextFactory,
|
||||
InvestApiClient investApiClient)
|
||||
{
|
||||
_botModeSwitcher = botModeSwitcher;
|
||||
_dataBus = dataBus;
|
||||
_provider = provider;
|
||||
_investApiClient = investApiClient;
|
||||
_managedAccountsNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_futureComission = options.Value.FutureComission;
|
||||
_shareComission = options.Value.ShareComission;
|
||||
_accountCashPart = options.Value.AccountCashPart;
|
||||
_accountCashPartFutures = options.Value.AccountCashPartFutures;
|
||||
_defaultBuyPartOfAccount = options.Value.DefaultBuyPartOfAccount;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns);
|
||||
var accountsList = new List<ManagedAccount>();
|
||||
int i = 0;
|
||||
foreach (var accountId in accounts)
|
||||
{
|
||||
var acc = _provider.GetKeyedService<ManagedAccount>(i);
|
||||
if (acc != null)
|
||||
{
|
||||
await acc.Init(accountId);
|
||||
Accounts[accountId] = acc;
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
|
||||
_ = ProcessMessages();
|
||||
}
|
||||
|
||||
|
||||
private async Task ProcessMessages()
|
||||
{
|
||||
while (await _pricesChannel.Reader.WaitToReadAsync())
|
||||
{
|
||||
var message = await _pricesChannel.Reader.ReadAsync();
|
||||
if (_historyCash.TryGetValue(message.Figi, out var data))
|
||||
{
|
||||
data.AddData(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = new PriceHistoryCacheUnit(message.Figi, message);
|
||||
_historyCash.TryAdd(message.Figi, data);
|
||||
}
|
||||
|
||||
if (message.IsHistoricalData)
|
||||
{
|
||||
float meanfullDiff;
|
||||
if (message.Figi == "BBG004730N88")
|
||||
{
|
||||
meanfullDiff = 0.16f;
|
||||
}
|
||||
else if (message.Figi == "FUTIMOEXF000")
|
||||
{
|
||||
meanfullDiff = 1.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var downtrendStarts = data.CheckDowntrendStarting(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(7), meanfullDiff);
|
||||
var uptrendStarts = data.CheckUptrendStarting(TimeSpan.FromSeconds(45), TimeSpan.FromSeconds(10), meanfullDiff);
|
||||
|
||||
var downtrendEnds = data.CheckDowntrendEnding(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(15), meanfullDiff);
|
||||
var uptrendEnds = data.CheckUptrendEnding(TimeSpan.FromSeconds(25), TimeSpan.FromSeconds(11), meanfullDiff);
|
||||
//var uptrendEnds2 = data.CheckUptrendEnding(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(20), meanfullDiff);
|
||||
|
||||
//var uptrendEnds = uptrendEnds1 || uptrendEnds2;
|
||||
|
||||
var declisionAction = DeclisionTradeAction.Unknown;
|
||||
|
||||
if (downtrendStarts)
|
||||
{
|
||||
//declisionAction = DeclisionTradeAction.OpenShort;
|
||||
}
|
||||
else if (uptrendStarts)
|
||||
{
|
||||
declisionAction = DeclisionTradeAction.OpenLong;
|
||||
}
|
||||
else if (downtrendEnds)
|
||||
{
|
||||
//declisionAction = DeclisionTradeAction.CloseShort;
|
||||
}
|
||||
else if(uptrendEnds)
|
||||
{
|
||||
declisionAction = DeclisionTradeAction.CloseLong;
|
||||
}
|
||||
|
||||
if (declisionAction != DeclisionTradeAction.Unknown)
|
||||
{
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
await context.Declisions.AddAsync(new Declision()
|
||||
{
|
||||
AccountId = string.Empty,
|
||||
Figi = message.Figi,
|
||||
Ticker = message.Ticker,
|
||||
Price = message.Value,
|
||||
Time = message.IsHistoricalData? message.Time: DateTime.UtcNow,
|
||||
Action = declisionAction,
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Preprocess(string figi)
|
||||
{
|
||||
if (_historyCash.TryGetValue(figi, out var unit))
|
||||
{
|
||||
var periodData1 = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, TimeSpan.FromSeconds(10));
|
||||
var periodData2 = unit.GetPriceDiffForTimeSpan(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
||||
if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff > 2)
|
||||
{
|
||||
//можно покупать.
|
||||
}
|
||||
|
||||
if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff < -2)
|
||||
{
|
||||
//можно продавать.
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private decimal GetComission(AssetType assetType)
|
||||
{
|
||||
if (assetType == AssetType.Common)
|
||||
{
|
||||
return _shareComission;
|
||||
}
|
||||
else if (assetType == AssetType.Futures)
|
||||
{
|
||||
return _futureComission;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private decimal GetCount(string accountId, decimal boutPrice)
|
||||
{
|
||||
var balance = Accounts[accountId].Balance;
|
||||
return System.Math.Floor(balance * _defaultBuyPartOfAccount / boutPrice);
|
||||
}
|
||||
|
||||
private bool IsBuyAllowed(string accountId, decimal boutPrice, decimal count, bool needBigCash)
|
||||
{
|
||||
if (!_botModeSwitcher.CanPurchase()) return false;
|
||||
|
||||
var balance = Accounts[accountId].Balance;
|
||||
var total = Accounts[accountId].Total;
|
||||
|
||||
var futures = Accounts[accountId].Assets.Values.FirstOrDefault(v => v.Type == AssetType.Futures);
|
||||
if (futures != null || needBigCash)
|
||||
{
|
||||
if ((balance - boutPrice * count) / total < _accountCashPartFutures) return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((balance - boutPrice * count) / total < _accountCashPart) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsSellAllowed(AssetType assetType, PositionType positionType, decimal boutPrice, decimal? requiredPrice, TradeCommandType commandType)
|
||||
{
|
||||
if (commandType >= TradeCommandType.MarketSell && commandType < TradeCommandType.ForceClosePosition && requiredPrice.HasValue)
|
||||
{
|
||||
var comission = GetComission(assetType);
|
||||
if (positionType == PositionType.Long)
|
||||
{
|
||||
return requiredPrice.Value * (1 - comission) > boutPrice * (1 + comission);
|
||||
}
|
||||
else if (positionType == PositionType.Short)
|
||||
{
|
||||
return requiredPrice.Value * (1 + comission) < boutPrice * (1 - comission);
|
||||
}
|
||||
}
|
||||
|
||||
if (commandType == TradeCommandType.ForceClosePosition) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
using KLHZ.Trader.Core.Declisions.Models;
|
||||
|
||||
namespace KLHZ.Trader.Core.Declisions.Utils
|
||||
{
|
||||
internal static class HistoryProcessingInstruments
|
||||
{
|
||||
internal static PeriodPricesInfo GetPriceDiffForTimeSpan(this PriceHistoryCacheUnit unit, TimeSpan timeShift, TimeSpan timeSpan)
|
||||
{
|
||||
var res = new PeriodPricesInfo(false, 0, 0, 0, 0, 0, timeSpan, 0);
|
||||
var data = unit.GetData();
|
||||
var times = data.timestamps;
|
||||
var prices = data.prices;
|
||||
if (times.Length < 2) return res;
|
||||
var lastPriceTime = times[times.Length - 1];
|
||||
var intervalEnd = lastPriceTime - timeShift;
|
||||
var intervalStart = intervalEnd - timeSpan;
|
||||
var max = float.MinValue;
|
||||
var min = float.MaxValue;
|
||||
|
||||
var intervaEndIndex = -1;
|
||||
var intervaStartIndex = -1;
|
||||
for (int i = times.Length - 1; i > -1; i--)
|
||||
{
|
||||
if (times[i] <= intervalEnd && intervaEndIndex < 0)
|
||||
{
|
||||
intervaEndIndex = i;
|
||||
}
|
||||
|
||||
if (prices[i] > max && intervaEndIndex >= 0)
|
||||
{
|
||||
max = prices[i];
|
||||
}
|
||||
if (prices[i] < min && intervaEndIndex >= 0)
|
||||
{
|
||||
min = prices[i];
|
||||
}
|
||||
if (times[i] <= intervalStart && intervaStartIndex < 0)
|
||||
{
|
||||
intervaStartIndex = i;
|
||||
if (intervaStartIndex != intervaEndIndex && intervaEndIndex >= 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (intervaStartIndex >= 0 && intervaEndIndex >= 0)
|
||||
{
|
||||
res = new PeriodPricesInfo(
|
||||
true,
|
||||
prices[intervaStartIndex],
|
||||
prices[intervaEndIndex],
|
||||
prices[intervaEndIndex] - prices[intervaStartIndex],
|
||||
min,
|
||||
max,
|
||||
timeSpan, intervaEndIndex - intervaStartIndex);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
internal static bool CheckStable(this PeriodPricesInfo data, float meanfullDiff)
|
||||
{
|
||||
meanfullDiff = Math.Abs(meanfullDiff);
|
||||
return data.Success && Math.Abs(data.PeriodDiff) < 1.5 * meanfullDiff && Math.Abs(data.PeriodMax - data.PeriodMin) < 2 * meanfullDiff;
|
||||
}
|
||||
|
||||
internal static bool CheckGrowing(this PeriodPricesInfo data, float meanfullDiff)
|
||||
{
|
||||
return meanfullDiff > 0 && data.Success && data.PeriodDiff > meanfullDiff && Math.Abs(data.PeriodMax - data.PeriodMin) < 3 * Math.Abs(data.PeriodDiff);
|
||||
}
|
||||
|
||||
internal static bool CheckFalling(this PeriodPricesInfo data, float meanfullDiff)
|
||||
{
|
||||
meanfullDiff = -meanfullDiff;
|
||||
return meanfullDiff < 0 && data.Success && data.PeriodDiff < meanfullDiff && Math.Abs(data.PeriodMax - data.PeriodMin) < 3 * Math.Abs(data.PeriodDiff);
|
||||
}
|
||||
|
||||
internal static float CalcTrendRelationAbs(PeriodPricesInfo first, PeriodPricesInfo second)
|
||||
{
|
||||
var k1 = Math.Abs(first.PeriodDiff) / Math.Abs(first.Period.TotalSeconds);
|
||||
var k2 = Math.Abs(second.PeriodDiff) / Math.Abs(second.Period.TotalSeconds);
|
||||
if (k2 == 0 && k1 != 0) return 1000;
|
||||
return (float)(k1 / k2);
|
||||
}
|
||||
|
||||
internal static bool CheckDowntrendEnding(this PriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff)
|
||||
{
|
||||
var totalDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, firstPeriod + secondPeriod);
|
||||
var startDiff = unit.GetPriceDiffForTimeSpan(secondPeriod, firstPeriod);
|
||||
var endDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, secondPeriod);
|
||||
var isEndStable = endDiff.CheckStable(meanfullDiff);
|
||||
var isEndGrown = endDiff.CheckGrowing(meanfullDiff);
|
||||
var isStartFalls = startDiff.CheckFalling(meanfullDiff);
|
||||
var isTotalFalls = totalDiff.CheckFalling(meanfullDiff);
|
||||
|
||||
var trendRelation = CalcTrendRelationAbs(startDiff, endDiff);
|
||||
var res = totalDiff.Success && isStartFalls && (isEndStable || isEndGrown) && trendRelation >= 2;
|
||||
|
||||
if (startDiff.Success)
|
||||
{
|
||||
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
internal static bool CheckUptrendEnding(this PriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff)
|
||||
{
|
||||
var totalDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, firstPeriod + secondPeriod);
|
||||
var startDiff = unit.GetPriceDiffForTimeSpan(secondPeriod, firstPeriod);
|
||||
var endDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, secondPeriod);
|
||||
var isEndStable = endDiff.CheckStable(meanfullDiff);
|
||||
var isEndFalls = endDiff.CheckFalling(meanfullDiff);
|
||||
var isStartGrows = startDiff.CheckGrowing(meanfullDiff);
|
||||
|
||||
var trendRelation = CalcTrendRelationAbs(startDiff, endDiff);
|
||||
var isEndLocal = endDiff.PeriodDiff == 0 && endDiff.Count == 2;
|
||||
var res = totalDiff.Success && isStartGrows && (isEndStable || isEndFalls) && (trendRelation >= 2 && !isEndLocal);
|
||||
|
||||
if (res)
|
||||
{
|
||||
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
internal static bool CheckDowntrendStarting(this PriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff)
|
||||
{
|
||||
var totalDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, firstPeriod + secondPeriod);
|
||||
var startDiff = unit.GetPriceDiffForTimeSpan(secondPeriod, firstPeriod);
|
||||
var endDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, secondPeriod);
|
||||
|
||||
|
||||
var isEndFalls = endDiff.CheckFalling(meanfullDiff);
|
||||
var isStartStable = startDiff.CheckStable(meanfullDiff);
|
||||
var isStartGrows = startDiff.CheckGrowing(meanfullDiff);
|
||||
|
||||
var trendRelation = CalcTrendRelationAbs(endDiff, startDiff);
|
||||
return totalDiff.Success && (isStartStable || isStartGrows) && isEndFalls && trendRelation >= 2;
|
||||
}
|
||||
|
||||
internal static bool CheckUptrendStarting(this PriceHistoryCacheUnit unit, TimeSpan firstPeriod, TimeSpan secondPeriod, float meanfullDiff)
|
||||
{
|
||||
var totalDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, firstPeriod + secondPeriod);
|
||||
var startDiff = unit.GetPriceDiffForTimeSpan(secondPeriod, firstPeriod);
|
||||
var endDiff = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, secondPeriod);
|
||||
|
||||
|
||||
var isEndGrows = endDiff.CheckGrowing(meanfullDiff);
|
||||
var isStartStable = startDiff.CheckStable(meanfullDiff);
|
||||
var isStartFalls = startDiff.CheckStable(meanfullDiff);
|
||||
|
||||
var trendRelation = CalcTrendRelationAbs(endDiff, startDiff);
|
||||
var res = totalDiff.Success && (isStartStable || isStartFalls) && isEndGrows && endDiff.PeriodDiff > meanfullDiff;
|
||||
|
||||
if (isStartStable)
|
||||
{
|
||||
res &= trendRelation >= 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
if (res)
|
||||
{
|
||||
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
namespace KLHZ.Trader.Core.Exchange
|
||||
{
|
||||
public class ExchangeConfig
|
||||
{
|
||||
public decimal FutureComission { get; set; }
|
||||
public decimal ShareComission { get; set; }
|
||||
public decimal AccountCashPart { get; set; }
|
||||
public decimal AccountCashPartFutures { get; set; }
|
||||
public decimal DefaultBuyPartOfAccount { get; set; }
|
||||
public string[] AllowedInstrumentsFigis { get; set; } = [];
|
||||
public string[] ManagingAccountNamePatterns { get; set; } = [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using Tinkoff.InvestApi;
|
||||
using Tinkoff.InvestApi.V1;
|
||||
|
||||
namespace KLHZ.Trader.Core.Exchange.Extentions
|
||||
{
|
||||
internal static class InvestApiClientExtentions
|
||||
{
|
||||
public static async Task<string[]> GetAccounts(this InvestApiClient client, params string[] managedAccountNamePatterns)
|
||||
{
|
||||
var accounts = await client.Users.GetAccountsAsync();
|
||||
var accsIds = new HashSet<string>();
|
||||
foreach (var pattern in managedAccountNamePatterns)
|
||||
{
|
||||
var aids = accounts.Accounts
|
||||
.Where(a => a.Name.ToLower().Contains(pattern) && a.AccessLevel == AccessLevel.AccountAccessLevelFullAccess)
|
||||
.Select(a => a.Id);
|
||||
foreach (var a in aids)
|
||||
{
|
||||
accsIds.Add(a);
|
||||
}
|
||||
}
|
||||
return accsIds.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using KLHZ.Trader.Core.Exchange.Models;
|
||||
|
||||
namespace KLHZ.Trader.Core.Exchange.Extentions
|
||||
{
|
||||
internal static class StringExtensions
|
||||
{
|
||||
internal static AssetType ParseInstrumentType(this string instrumentType)
|
||||
{
|
||||
switch (instrumentType)
|
||||
{
|
||||
case "futures":
|
||||
return AssetType.Futures;
|
||||
case "currency":
|
||||
return AssetType.Currency;
|
||||
case "share":
|
||||
return AssetType.Common;
|
||||
default:
|
||||
return AssetType.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
namespace KLHZ.Trader.Core.Exchange.Models
|
||||
{
|
||||
public class Asset
|
||||
{
|
||||
public decimal BlockedItems { get; init; }
|
||||
public AssetType Type { get; init; }
|
||||
public PositionType Position { get; init; }
|
||||
public DateTime BoughtAt { get; init; }
|
||||
public required string AccountId { get; init; }
|
||||
public required string Figi { get; init; }
|
||||
public required string Ticker { get; init; }
|
||||
public decimal BoughtPrice { get; init; }
|
||||
public decimal Count { get; init; }
|
||||
public decimal CountLots { get; init; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
namespace KLHZ.Trader.Core.Exchange.Models
|
||||
{
|
||||
public enum AssetType
|
||||
{
|
||||
Unknown = 0,
|
||||
Currency = 1,
|
||||
Common = 2,
|
||||
Futures = 3,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace KLHZ.Trader.Core.Exchange.Models
|
||||
{
|
||||
public class DealResult
|
||||
{
|
||||
public decimal Price { get; set; }
|
||||
public decimal Count { get; set; }
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace KLHZ.Trader.Core.Exchange.Models
|
||||
{
|
||||
public enum PositionType
|
||||
{
|
||||
Unknown = 0,
|
||||
Long = 1,
|
||||
Short = 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
using Grpc.Core;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.DataLayer;
|
||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||
using KLHZ.Trader.Core.Exchange.Extentions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using Tinkoff.InvestApi;
|
||||
using Tinkoff.InvestApi.V1;
|
||||
using Candle = KLHZ.Trader.Core.DataLayer.Entities.Prices.Candle;
|
||||
|
||||
namespace KLHZ.Trader.Core.Exchange.Services
|
||||
{
|
||||
public class ExchangeDataReader : IHostedService
|
||||
{
|
||||
private readonly InvestApiClient _investApiClient;
|
||||
private readonly string[] _instrumentsFigis = [];
|
||||
private readonly string[] _managedAccountNamePatterns;
|
||||
private readonly ILogger<ExchangeDataReader> _logger;
|
||||
private readonly ConcurrentDictionary<string, string> _tickersCache = new();
|
||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly IDataBus _eventBus;
|
||||
|
||||
public ExchangeDataReader(InvestApiClient investApiClient, IDataBus eventBus,
|
||||
IOptions<ExchangeConfig> options, IDbContextFactory<TraderDbContext> dbContextFactory,
|
||||
ILogger<ExchangeDataReader> logger)
|
||||
{
|
||||
_eventBus = eventBus;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_investApiClient = investApiClient;
|
||||
_instrumentsFigis = options.Value.AllowedInstrumentsFigis.ToArray();
|
||||
_logger = logger;
|
||||
_managedAccountNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Инициализация приемника данных с биржи");
|
||||
var accounts = await _investApiClient.GetAccounts(_managedAccountNamePatterns);
|
||||
await InitCache();
|
||||
_ = CycleSubscribtion(accounts);
|
||||
}
|
||||
|
||||
private async Task InitCache()
|
||||
{
|
||||
var shares = await _investApiClient.Instruments.SharesAsync();
|
||||
foreach (var share in shares.Instruments)
|
||||
{
|
||||
//if (_instrumentsFigis.Contains(share.Figi))
|
||||
{
|
||||
_tickersCache.TryAdd(share.Figi, share.Ticker);
|
||||
}
|
||||
}
|
||||
var futures = await _investApiClient.Instruments.FuturesAsync();
|
||||
foreach (var future in futures.Instruments)
|
||||
{
|
||||
//if (_instrumentsFigis.Contains(future.Figi))
|
||||
{
|
||||
_tickersCache.TryAdd(future.Figi, future.Ticker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CycleSubscribtion(string[] accounts)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
//await SubscribePrices();
|
||||
await Task.Delay(1000);
|
||||
//await SubscribeCandles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка в одном из стримов получения данных от биржи.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubscribePrices()
|
||||
{
|
||||
using var stream = _investApiClient.MarketDataStream.MarketDataStream();
|
||||
|
||||
var request = new SubscribeLastPriceRequest
|
||||
{
|
||||
SubscriptionAction = SubscriptionAction.Subscribe
|
||||
};
|
||||
|
||||
foreach (var f in _instrumentsFigis)
|
||||
{
|
||||
request.Instruments.Add(
|
||||
new LastPriceInstrument()
|
||||
{
|
||||
InstrumentId = f
|
||||
});
|
||||
}
|
||||
|
||||
await stream.RequestStream.WriteAsync(new MarketDataRequest
|
||||
{
|
||||
SubscribeLastPriceRequest = request,
|
||||
});
|
||||
|
||||
await foreach (var response in stream.ResponseStream.ReadAllAsync())
|
||||
{
|
||||
if (response.LastPrice != null)
|
||||
{
|
||||
var message = new PriceChange()
|
||||
{
|
||||
Figi = response.LastPrice.Figi,
|
||||
Ticker = GetTickerByFigi(response.LastPrice.Figi),
|
||||
Time = response.LastPrice.Time.ToDateTime().ToUniversalTime(),
|
||||
Value = response.LastPrice.Price,
|
||||
IsHistoricalData = false,
|
||||
};
|
||||
await _eventBus.BroadcastNewPrice(message);
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
await context.PriceChanges.AddAsync(message);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubscribeCandles()
|
||||
{
|
||||
using var stream = _investApiClient.MarketDataStream.MarketDataStream();
|
||||
|
||||
var request = new SubscribeCandlesRequest
|
||||
{
|
||||
SubscriptionAction = SubscriptionAction.Subscribe,
|
||||
CandleSourceType = GetCandlesRequest.Types.CandleSource.Exchange
|
||||
};
|
||||
|
||||
foreach (var f in _instrumentsFigis)
|
||||
{
|
||||
request.Instruments.Add(
|
||||
new CandleInstrument()
|
||||
{
|
||||
InstrumentId = f,
|
||||
Interval = SubscriptionInterval.OneMinute
|
||||
});
|
||||
}
|
||||
|
||||
await stream.RequestStream.WriteAsync(new MarketDataRequest
|
||||
{
|
||||
SubscribeCandlesRequest = request,
|
||||
});
|
||||
|
||||
await foreach (var response in stream.ResponseStream.ReadAllAsync())
|
||||
{
|
||||
if (response.Candle != null)
|
||||
{
|
||||
var message = new Candle()
|
||||
{
|
||||
Figi = response.Candle.Figi,
|
||||
Ticker = GetTickerByFigi(response.LastPrice.Figi),
|
||||
Time = response.Candle.Time.ToDateTime().ToUniversalTime(),
|
||||
Close = response.Candle.Close,
|
||||
Open = response.Candle.Open,
|
||||
Low = response.Candle.Low,
|
||||
High = response.Candle.High,
|
||||
Volume = response.Candle.Volume,
|
||||
IsHistoricalData = false,
|
||||
};
|
||||
await _eventBus.BroadcastNewCandle(message);
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
await context.Candles.AddAsync(message);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
private string GetTickerByFigi(string figi)
|
||||
{
|
||||
return _tickersCache.TryGetValue(figi, out var ticker) ? ticker : string.Empty;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages.Enums;
|
||||
using KLHZ.Trader.Core.DataLayer;
|
||||
using KLHZ.Trader.Core.Exchange.Extentions;
|
||||
using KLHZ.Trader.Core.Exchange.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
using Tinkoff.InvestApi;
|
||||
using Tinkoff.InvestApi.V1;
|
||||
using PositionType = KLHZ.Trader.Core.Exchange.Models.PositionType;
|
||||
|
||||
namespace KLHZ.Trader.Core.Exchange.Services
|
||||
{
|
||||
public class ManagedAccount
|
||||
{
|
||||
public string AccountId { get; private set; } = string.Empty;
|
||||
private readonly Channel<TradeCommand> _channel = Channel.CreateUnbounded<TradeCommand>();
|
||||
|
||||
#region Поля, собираемые из контейнера DI
|
||||
private readonly InvestApiClient _investApiClient;
|
||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||
private readonly ILogger<ManagedAccount> _logger;
|
||||
private readonly IDataBus _dataBus;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Кеш рабочих данных
|
||||
private readonly object _locker = new();
|
||||
private decimal _balance = 0;
|
||||
private decimal _total = 0;
|
||||
internal decimal Balance
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_locker)
|
||||
return _balance;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (_locker)
|
||||
_balance = value;
|
||||
}
|
||||
}
|
||||
internal decimal Total
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_locker)
|
||||
return _total;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (_locker)
|
||||
_total = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly ConcurrentDictionary<string, Models.Asset> Assets = new();
|
||||
|
||||
#endregion
|
||||
|
||||
public ManagedAccount(InvestApiClient investApiClient, IDataBus dataBus, IDbContextFactory<TraderDbContext> dbContextFactory, ILogger<ManagedAccount> logger)
|
||||
{
|
||||
_dataBus = dataBus;
|
||||
_investApiClient = investApiClient;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Init(string accountId)
|
||||
{
|
||||
AccountId = accountId;
|
||||
await SyncPortfolio();
|
||||
_dataBus.AddChannel(accountId, _channel);
|
||||
_ = ProcessCommands();
|
||||
}
|
||||
|
||||
private async Task ProcessCommands()
|
||||
{
|
||||
while (await _channel.Reader.WaitToReadAsync())
|
||||
{
|
||||
var command = await _channel.Reader.ReadAsync();
|
||||
try
|
||||
{
|
||||
await ProcessMarketCommand(command);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка при обработке команды.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task SyncPortfolio()
|
||||
{
|
||||
try
|
||||
{
|
||||
//await _semaphoreSlim.WaitAsync();
|
||||
var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest()
|
||||
{
|
||||
AccountId = AccountId,
|
||||
});
|
||||
|
||||
var oldAssets = Assets.Keys.ToHashSet();
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
var trades = await context.Trades
|
||||
.Where(t => t.AccountId == AccountId && t.ArchiveStatus == 0)
|
||||
.ToListAsync();
|
||||
foreach (var position in portfolio.Positions)
|
||||
{
|
||||
decimal price = 0;
|
||||
var trade = trades.FirstOrDefault(t => t.Figi == position.Figi);
|
||||
|
||||
if (trade != null)
|
||||
{
|
||||
trades.Remove(trade);
|
||||
price = trade.Price;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = position.AveragePositionPrice;
|
||||
}
|
||||
#pragma warning disable CS0612 // Тип или член устарел
|
||||
var asset = new Models.Asset()
|
||||
{
|
||||
AccountId = AccountId,
|
||||
Figi = position.Figi,
|
||||
Ticker = position.Ticker,
|
||||
BoughtAt = trade?.BoughtAt ?? DateTime.UtcNow,
|
||||
BoughtPrice = price,
|
||||
Type = position.InstrumentType.ParseInstrumentType(),
|
||||
Position = position.Quantity > 0 ? PositionType.Long : PositionType.Short,
|
||||
BlockedItems = position.BlockedLots,
|
||||
Count = position.Quantity,
|
||||
CountLots = position.QuantityLots,
|
||||
};
|
||||
#pragma warning restore CS0612 // Тип или член устарел
|
||||
Assets.AddOrUpdate(asset.Figi, asset, (k, v) => asset);
|
||||
oldAssets.Remove(asset.Figi);
|
||||
}
|
||||
|
||||
Total = portfolio.TotalAmountPortfolio;
|
||||
Balance = portfolio.TotalAmountCurrencies;
|
||||
|
||||
foreach (var asset in oldAssets)
|
||||
{
|
||||
Assets.TryRemove(asset, out _);
|
||||
}
|
||||
|
||||
var ids = trades.Select(t => t.Id).ToArray();
|
||||
await context.Trades
|
||||
.Where(t => ids.Contains(t.Id))
|
||||
.ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", AccountId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
//_semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal async Task<DealResult> ClosePosition(string figi)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(figi) && Assets.TryGetValue(figi, out var asset))
|
||||
{
|
||||
try
|
||||
{
|
||||
var req = new PostOrderRequest()
|
||||
{
|
||||
AccountId = AccountId,
|
||||
InstrumentId = figi,
|
||||
};
|
||||
if (asset != null)
|
||||
{
|
||||
req.Direction = OrderDirection.Sell;
|
||||
req.OrderType = OrderType.Market;
|
||||
req.Quantity = (long)asset.Count;
|
||||
|
||||
var res = await _investApiClient.Orders.PostOrderAsync(req);
|
||||
return new DealResult
|
||||
{
|
||||
Count = res.LotsExecuted,
|
||||
Price = res.ExecutedOrderPrice,
|
||||
Success = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка при закрытии позиции по счёту {acc}. figi: {figi}", AccountId, figi);
|
||||
}
|
||||
}
|
||||
return new DealResult
|
||||
{
|
||||
Count = 0,
|
||||
Price = 0,
|
||||
Success = false,
|
||||
};
|
||||
}
|
||||
|
||||
internal async Task<DealResult> BuyAsset(string figi, decimal count, string? ticker = null, decimal? recommendedPrice = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var req = new PostOrderRequest()
|
||||
{
|
||||
AccountId = AccountId,
|
||||
InstrumentId = figi,
|
||||
Direction = OrderDirection.Buy,
|
||||
OrderType = OrderType.Market,
|
||||
Quantity = (long)count,
|
||||
};
|
||||
|
||||
var res = await _investApiClient.Orders.PostOrderAsync(req);
|
||||
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
var trade = await context.Trades.FirstOrDefaultAsync(t => t.ArchiveStatus == 0 && t.Figi == figi);
|
||||
if (trade == null)
|
||||
{
|
||||
var newTrade = new DataLayer.Entities.Trades.Trade()
|
||||
{
|
||||
AccountId = AccountId,
|
||||
Figi = figi,
|
||||
Ticker = ticker ?? string.Empty,
|
||||
BoughtAt = DateTime.UtcNow,
|
||||
Count = res.LotsExecuted,
|
||||
Price = res.ExecutedOrderPrice,
|
||||
Position = DataLayer.Entities.Trades.PositionType.Long,
|
||||
Direction = DataLayer.Entities.Trades.TradeDirection.Income,
|
||||
Asset = DataLayer.Entities.Trades.AssetType.Common,
|
||||
};
|
||||
|
||||
await context.Trades.AddAsync(newTrade);
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldAmount = trade.Price * trade.Count;
|
||||
var newAmount = res.ExecutedOrderPrice * res.LotsExecuted;
|
||||
trade.Count = res.LotsExecuted + trade.Count;
|
||||
trade.Price = (oldAmount + newAmount) / trade.Count;
|
||||
context.Trades.Update(trade);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
return new DealResult
|
||||
{
|
||||
Count = res.LotsExecuted,
|
||||
Price = res.ExecutedOrderPrice,
|
||||
Success = true,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка при покупке актива на счёт {acc}. figi: {figi}", AccountId, figi);
|
||||
}
|
||||
return new DealResult
|
||||
{
|
||||
Count = 0,
|
||||
Price = 0,
|
||||
Success = false,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ProcessMarketCommand(TradeCommand command)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(command.Figi)) return;
|
||||
if (command.CommandType == TradeCommandType.MarketBuy)
|
||||
{
|
||||
await BuyAsset(command.Figi, command.Count ?? 1, command.Ticker, command.RecomendPrice);
|
||||
}
|
||||
else if (command.CommandType == TradeCommandType.ForceClosePosition)
|
||||
{
|
||||
await ClosePosition(command.Figi);
|
||||
}
|
||||
else return;
|
||||
|
||||
await SyncPortfolio();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Telegram.Bot" Version="22.6.0" />
|
||||
<PackageReference Include="Tinkoff.InvestApi" Version="0.6.17" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,123 @@
|
|||
using KLHZ.Trader.Core.Common;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Immutable;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
namespace KLHZ.Trader.Core.TG.Services
|
||||
{
|
||||
public class BotMessagesHandler : IUpdateHandler
|
||||
{
|
||||
private readonly ImmutableArray<long> _admins = [];
|
||||
private readonly BotModeSwitcher _botModeSwitcher;
|
||||
private readonly IDataBus _eventBus;
|
||||
private readonly ILogger<BotMessagesHandler> _logger;
|
||||
public BotMessagesHandler(BotModeSwitcher botModeSwitcher, IDataBus eventBus, IOptions<TgBotConfig> options, ILogger<BotMessagesHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_botModeSwitcher = botModeSwitcher;
|
||||
_eventBus = eventBus;
|
||||
_admins = ImmutableArray.CreateRange(options.Value.Admins);
|
||||
}
|
||||
|
||||
public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (update.Message != null && update.Message?.From != null && _admins.Contains(update.Message.From.Id))
|
||||
{
|
||||
switch (update.Message.Text)
|
||||
{
|
||||
|
||||
case "/start":
|
||||
{
|
||||
var replyKeyboardMarkup = new ReplyKeyboardMarkup(new[] {
|
||||
new KeyboardButton[] { Constants.BotCommandsButtons.EnableSelling, Constants.BotCommandsButtons.DisableSelling},
|
||||
new KeyboardButton[] { Constants.BotCommandsButtons.EnablePurchases, Constants.BotCommandsButtons.DisablePurchases}});
|
||||
|
||||
|
||||
await botClient.SendMessage(update.Message.Chat, "Принято!", replyMarkup: replyKeyboardMarkup);
|
||||
break;
|
||||
}
|
||||
case Constants.BotCommandsButtons.EnableSelling:
|
||||
{
|
||||
_botModeSwitcher.StartSelling();
|
||||
await botClient.SendMessage(update.Message.Chat, "Продажи начаты!");
|
||||
break;
|
||||
}
|
||||
case Constants.BotCommandsButtons.DisableSelling:
|
||||
{
|
||||
_botModeSwitcher.StopSelling();
|
||||
await botClient.SendMessage(update.Message.Chat, "Продажи остановлены!");
|
||||
break;
|
||||
}
|
||||
case Constants.BotCommandsButtons.EnablePurchases:
|
||||
{
|
||||
_botModeSwitcher.StartPurchase();
|
||||
await botClient.SendMessage(update.Message.Chat, "Покупки начаты!");
|
||||
break;
|
||||
}
|
||||
case Constants.BotCommandsButtons.DisablePurchases:
|
||||
{
|
||||
_botModeSwitcher.StopPurchase();
|
||||
await botClient.SendMessage(update.Message.Chat, "Покупки остановлены!");
|
||||
break;
|
||||
}
|
||||
case "сбросить сбер":
|
||||
{
|
||||
var command = new TradeCommand()
|
||||
{
|
||||
CommandType = TradeCommandType.ForceClosePosition,
|
||||
RecomendPrice = null,
|
||||
Figi = "BBG004730N88",
|
||||
};
|
||||
await _eventBus.BroadcastCommand(command);
|
||||
break;
|
||||
}
|
||||
case "продать сбер":
|
||||
{
|
||||
var command = new TradeCommand()
|
||||
{
|
||||
CommandType = TradeCommandType.MarketSell,
|
||||
RecomendPrice = null,
|
||||
Figi = "BBG004730N88",
|
||||
Count = 1,
|
||||
LotsCount = 1,
|
||||
};
|
||||
await _eventBus.BroadcastCommand(command);
|
||||
break;
|
||||
}
|
||||
case "купить сбер":
|
||||
{
|
||||
var command = new TradeCommand()
|
||||
{
|
||||
CommandType = TradeCommandType.MarketBuy,
|
||||
RecomendPrice = null,
|
||||
Figi = "BBG004730N88",
|
||||
Count = 1
|
||||
};
|
||||
await _eventBus.BroadcastCommand(command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
await botClient.SendMessage(update.Message.Chat, "Принято!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Ошибка при обработке сообщения из телеграма.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Channels;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Polling;
|
||||
|
||||
namespace KLHZ.Trader.Core.TG.Services
|
||||
{
|
||||
public class BotStarter : IHostedService
|
||||
{
|
||||
private readonly TelegramBotClient _botClient;
|
||||
private readonly IUpdateHandler _updateHandler;
|
||||
private readonly Channel<MessageForAdmin> _messages = Channel.CreateUnbounded<MessageForAdmin>();
|
||||
private readonly ImmutableArray<long> _admins = [];
|
||||
|
||||
public BotStarter(IOptions<TgBotConfig> cfg, IUpdateHandler updateHandler, IDataBus dataBus, IOptions<TgBotConfig> options)
|
||||
{
|
||||
_botClient = new TelegramBotClient(cfg.Value.Token);
|
||||
_updateHandler = updateHandler;
|
||||
dataBus.AddChannel(_messages);
|
||||
_admins = ImmutableArray.CreateRange(options.Value.Admins);
|
||||
_ = ProcessMessages();
|
||||
}
|
||||
|
||||
private async Task ProcessMessages()
|
||||
{
|
||||
while (await _messages.Reader.WaitToReadAsync())
|
||||
{
|
||||
var message = await _messages.Reader.ReadAsync();
|
||||
foreach (var admin in _admins)
|
||||
{
|
||||
await _botClient.SendMessage(admin, message.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_botClient.StartReceiving(_updateHandler);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _botClient.Close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace KLHZ.Trader.Core.TG
|
||||
{
|
||||
public class TgBotConfig
|
||||
{
|
||||
public required string Token { get; set; }
|
||||
|
||||
public required long[] Admins = [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
using Google.Protobuf.WellKnownTypes;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using KLHZ.Trader.Core.DataLayer;
|
||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using Tinkoff.InvestApi;
|
||||
using Tinkoff.InvestApi.V1;
|
||||
using Candle = KLHZ.Trader.Core.DataLayer.Entities.Prices.Candle;
|
||||
|
||||
namespace KLHZ.Trader.HistoryLoader.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class LoaderController : ControllerBase
|
||||
{
|
||||
private readonly InvestApiClient _investApiClient;
|
||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||
|
||||
public LoaderController(InvestApiClient client, IDbContextFactory<TraderDbContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_investApiClient = client;
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task Load(string figi)
|
||||
{
|
||||
using var context1 = await _dbContextFactory.CreateDbContextAsync();
|
||||
context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var existed = (await context1.Candles.Where(c => c.Figi == figi).Select(c => c.Time)
|
||||
.ToArrayAsync()).Select(t => t.ToUniversalTime())
|
||||
.ToHashSet();
|
||||
var dt = DateTime.UtcNow;
|
||||
var i = -400;
|
||||
while (i < 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var req = new GetCandlesRequest()
|
||||
{
|
||||
Interval = CandleInterval._5Sec,
|
||||
From = Timestamp.FromDateTime(dt.AddHours(i)),
|
||||
To = Timestamp.FromDateTime(dt.AddHours(i + 1)),
|
||||
InstrumentId = figi,
|
||||
};
|
||||
var candles = await _investApiClient.MarketData.GetCandlesAsync(req);
|
||||
i++;
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var forAdd = new List<Candle>();
|
||||
foreach (var c in candles.Candles)
|
||||
{
|
||||
var dt1 = c.Time.ToDateTime().ToUniversalTime();
|
||||
if (!existed.Contains(dt1))
|
||||
{
|
||||
var ca = new Candle
|
||||
{
|
||||
Figi = figi,
|
||||
Ticker = string.Empty,
|
||||
Time = dt1,
|
||||
Open = c.Open,
|
||||
Close = c.Close,
|
||||
Volume = c.Volume,
|
||||
High = c.High,
|
||||
Low = c.Low,
|
||||
};
|
||||
forAdd.Add(ca);
|
||||
existed.Add(dt1);
|
||||
}
|
||||
}
|
||||
await context.Candles.AddRangeAsync(forAdd);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки.
|
||||
|
||||
# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
# Этот этап используется для сборки проекта службы
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["KLHZ.Trader.HistoryLoader/KLHZ.Trader.HistoryLoader.csproj", "KLHZ.Trader.HistoryLoader/"]
|
||||
COPY ["KLHZ.Trader.Core/KLHZ.Trader.Core.csproj", "KLHZ.Trader.Core/"]
|
||||
RUN dotnet restore "./KLHZ.Trader.HistoryLoader/KLHZ.Trader.HistoryLoader.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/KLHZ.Trader.HistoryLoader"
|
||||
RUN dotnet build "./KLHZ.Trader.HistoryLoader.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./KLHZ.Trader.HistoryLoader.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "KLHZ.Trader.HistoryLoader.dll"]
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KLHZ.Trader.Core\KLHZ.Trader.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,30 @@
|
|||
using KLHZ.Trader.Core.DataLayer;
|
||||
using KLHZ.Trader.Core.Exchange;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddInvestApiClient((_, settings) =>
|
||||
{
|
||||
settings.AccessToken = builder.Configuration.GetSection(nameof(ExchangeConfig))["Token"];
|
||||
});
|
||||
|
||||
builder.Services.AddDbContextFactory<TraderDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("PostgresConnection"));
|
||||
});
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -0,0 +1,696 @@
|
|||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Торговля сбером",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 1,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "points",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 9,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"fieldMinMax": false,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "value"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.drawStyle",
|
||||
"value": "line"
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "light-green",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "OpenLong"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "super-light-yellow",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "CloseLong"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "dark-yellow",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "OpenShort"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "super-light-red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "CloseShort"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "dark-red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"maxDataPoints": 500,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, value FROM price_changes WHERE figi ='BBG004730N88' ORDER BY time desc LIMIT 500",
|
||||
"refId": "A",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "functionParameter"
|
||||
}
|
||||
],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"table": "price_changes"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"OpenLong\" FROM declisions WHERE figi ='BBG004730N88' AND action =100 ORDER BY time desc LIMIT 500",
|
||||
"refId": "B",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "price",
|
||||
"type": "functionParameter"
|
||||
}
|
||||
],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"table": "declisions"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"CloseLong\" FROM declisions WHERE figi ='BBG004730N88' AND action =200 ORDER BY time desc LIMIT 500",
|
||||
"refId": "C",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"OpenShort\" FROM declisions WHERE figi ='BBG004730N88' AND action =300 ORDER BY time desc LIMIT 500",
|
||||
"refId": "D",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"CloseShort\" FROM declisions WHERE figi ='BBG004730N88' AND action =400 ORDER BY time desc LIMIT 500",
|
||||
"refId": "E",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "SBER",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "points",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 9,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"fieldMinMax": false,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "value"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.drawStyle",
|
||||
"value": "line"
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "light-green",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "OpenLong"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "super-light-yellow",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "CloseLong"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "dark-yellow",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "OpenShort"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "super-light-red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "CloseShort"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "dark-red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 13
|
||||
},
|
||||
"id": 2,
|
||||
"maxDataPoints": 500,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, value FROM price_changes WHERE figi ='FUTIMOEXF000' ORDER BY time desc LIMIT 500",
|
||||
"refId": "A",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "functionParameter"
|
||||
}
|
||||
],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"table": "price_changes"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"OpenLong\" FROM declisions WHERE figi ='FUTIMOEXF000' AND action =100 ORDER BY time desc LIMIT 500",
|
||||
"refId": "B",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "price",
|
||||
"type": "functionParameter"
|
||||
}
|
||||
],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"table": "declisions"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"CloseLong\" FROM declisions WHERE figi ='FUTIMOEXF000' AND action =200 ORDER BY time desc LIMIT 500",
|
||||
"refId": "C",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"OpenShort\" FROM declisions WHERE figi ='FUTIMOEXF000' AND action =300 ORDER BY time desc LIMIT 500",
|
||||
"refId": "D",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "bew2oc42looowc"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time, price as \"CloseShort\" FROM declisions WHERE figi ='FUTIMOEXF000' AND action =400 ORDER BY time desc LIMIT 500",
|
||||
"refId": "E",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "IMOEXF",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 41,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "SBER",
|
||||
"uid": "25359e73-1645-466b-9e1e-b9dbc4742a41",
|
||||
"version": 12
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
auth_enabled: false
|
||||
|
||||
server:
|
||||
http_listen_port: 3100
|
||||
grpc_listen_port: 9096
|
||||
log_level: info
|
||||
grpc_server_max_concurrent_streams: 1000
|
||||
|
||||
common:
|
||||
instance_addr: 127.0.0.1
|
||||
path_prefix: /tmp/loki
|
||||
storage:
|
||||
filesystem:
|
||||
chunks_directory: /tmp/loki/chunks
|
||||
rules_directory: /tmp/loki/rules
|
||||
replication_factor: 1
|
||||
ring:
|
||||
kvstore:
|
||||
store: inmemory
|
||||
|
||||
query_range:
|
||||
results_cache:
|
||||
cache:
|
||||
embedded_cache:
|
||||
enabled: true
|
||||
max_size_mb: 100
|
||||
|
||||
limits_config:
|
||||
metric_aggregation_enabled: true
|
||||
allow_structured_metadata: true
|
||||
volume_enabled: true
|
||||
retention_period: 24h # 24h
|
||||
|
||||
schema_config:
|
||||
configs:
|
||||
- from: 2020-10-24
|
||||
store: tsdb
|
||||
object_store: filesystem
|
||||
schema: v13
|
||||
index:
|
||||
prefix: index_
|
||||
period: 24h
|
||||
|
||||
pattern_ingester:
|
||||
enabled: true
|
||||
metric_aggregation:
|
||||
loki_address: localhost:3100
|
||||
|
||||
ruler:
|
||||
enable_alertmanager_discovery: true
|
||||
enable_api: true
|
||||
|
||||
|
||||
|
||||
|
||||
frontend:
|
||||
encoding: protobuf
|
||||
|
||||
|
||||
compactor:
|
||||
working_directory: /tmp/loki/retention
|
||||
delete_request_store: filesystem
|
||||
retention_enabled: true
|
|
@ -0,0 +1,49 @@
|
|||
drop table if exists trades;
|
||||
create table trades
|
||||
(
|
||||
trade_id bigserial,
|
||||
account_id text not null,
|
||||
bought_at timestamp default current_timestamp,
|
||||
figi text not null,
|
||||
ticker text not null,
|
||||
price decimal not null,
|
||||
count decimal not null,
|
||||
count_lots decimal not null,
|
||||
archive_status int not null default 0,
|
||||
direction int not null default 1,
|
||||
position_type int not null default 1,
|
||||
asset_type int not null default 1,
|
||||
primary key (trade_id,archive_status)
|
||||
) partition by list (archive_status);
|
||||
|
||||
create table assets_active partition of trades for values in (0);
|
||||
create table assets_archive partition of trades for values in (1);
|
||||
|
||||
CREATE INDEX assets_account_id_index ON trades USING btree(account_id);
|
||||
CREATE INDEX assets_figi_index ON trades USING btree(figi);
|
||||
drop table if exists price_changes;
|
||||
create table price_changes
|
||||
(
|
||||
id bigserial,
|
||||
time timestamp default current_timestamp,
|
||||
figi text not null,
|
||||
ticker text not null,
|
||||
value decimal not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
CREATE INDEX price_changes_figi_index ON price_changes USING btree(figi, time);
|
||||
drop table if exists declisions;
|
||||
create table declisions
|
||||
(
|
||||
id bigserial,
|
||||
time timestamp default current_timestamp,
|
||||
figi text not null,
|
||||
price decimal not null,
|
||||
ticker text not null,
|
||||
account_id text not null,
|
||||
action int not null default 0,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
CREATE INDEX declisions_index ON declisions USING btree(figi, time);
|
|
@ -0,0 +1,7 @@
|
|||
scrape_configs:
|
||||
- job_name: 'trader'
|
||||
metrics_path: /metrics
|
||||
scrape_interval: 5s
|
||||
static_configs:
|
||||
- targets: ['klhz_trader:8080','gateway.docker.internal:9100']
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using Google.Protobuf.WellKnownTypes;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
|
||||
using KLHZ.Trader.Core.DataLayer;
|
||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using Tinkoff.InvestApi;
|
||||
using Tinkoff.InvestApi.V1;
|
||||
using Candle = KLHZ.Trader.Core.DataLayer.Entities.Prices.Candle;
|
||||
|
||||
namespace KLHZ.Trader.Service.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class PlayController : ControllerBase
|
||||
{
|
||||
private readonly IDataBus _dataBus;
|
||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||
|
||||
public PlayController(IDataBus dataBus, IDbContextFactory<TraderDbContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_dataBus = dataBus;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task Run(string figi)
|
||||
{
|
||||
using var context1 = await _dbContextFactory.CreateDbContextAsync();
|
||||
context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var data = await context1.Candles
|
||||
.Where(c => c.Figi == figi)
|
||||
.OrderBy(c=>c.Time)
|
||||
.Select(c => new NewPriceMessage()
|
||||
{
|
||||
Figi = figi,
|
||||
Ticker = c.Ticker,
|
||||
Time = c.Time,
|
||||
Value = c.Close,
|
||||
IsHistoricalData = true
|
||||
})
|
||||
.ToArrayAsync();
|
||||
|
||||
foreach (var mess in data)
|
||||
{
|
||||
await _dataBus.BroadcastNewPrice(mess);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки.
|
||||
|
||||
# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
# Этот этап используется для сборки проекта службы
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["KLHZ.Trader.Service/KLHZ.Trader.Service.csproj", "KLHZ.Trader.Service/"]
|
||||
COPY ["KLHZ.Trader.Core/KLHZ.Trader.Core.csproj", "KLHZ.Trader.Core/"]
|
||||
|
||||
RUN dotnet restore "./KLHZ.Trader.Service/KLHZ.Trader.Service.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/KLHZ.Trader.Service"
|
||||
RUN dotnet build "./KLHZ.Trader.Service.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./KLHZ.Trader.Service.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "KLHZ.Trader.Service.dll"]
|
|
@ -0,0 +1,82 @@
|
|||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Filters;
|
||||
using Serilog.Sinks.Grafana.Loki;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace KLHZ.Trader.Service.Infrastructure
|
||||
{
|
||||
internal static class IHostBuilderExtensions
|
||||
{
|
||||
private readonly static Regex urlCheckRegex = new("^http://|https://.+");
|
||||
private readonly static ImmutableArray<string> DefaultLabels = new List<string>()
|
||||
{
|
||||
"action",
|
||||
}.ToImmutableArray();
|
||||
|
||||
/// <summary>
|
||||
/// Добавить логирование в Loki
|
||||
/// </summary>
|
||||
/// <param name="hostBuilder"></param>
|
||||
/// <param name="lokiUrl">url loki, куда писать логи.</param>
|
||||
/// <param name="serviceName">Название сервиса, от имени которого пишутся логи. Если передан null - берется название домена. </param>
|
||||
/// <param name="writeToConsole">Флаг, включающий дублирование логов в консоль.</param>
|
||||
/// <param name="minLevel">Минимальный уровень логирования приложения.</param>
|
||||
/// <param name="EFMinLogLevel">Минимальный уровень логирования для EF.</param>
|
||||
/// <param name="excludeEFLogs">Удалять из логов записи, сгенерированные EF.</param>
|
||||
/// <param name="excludeMetricsScrapingLogs">Удалять из логов записи, возникшие в результате скреппинга метрик прометеусом..</param>
|
||||
/// <param name="additionalFiltratonLabels">Набор тегов, который будет извлекаться при отправке в Loki
|
||||
/// и помечаться как Label для ускорения поиска по логам.</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static IHostBuilder ConfigureSerilog(this IHostBuilder hostBuilder, string? lokiUrl,
|
||||
string? serviceName = null,
|
||||
bool writeToConsole = false,
|
||||
List<string>? additionalFiltratonLabels = null,
|
||||
LogEventLevel minLevel = LogEventLevel.Information,
|
||||
LogEventLevel EFMinLogLevel = LogEventLevel.Information,
|
||||
bool excludeEFLogs = true,
|
||||
bool excludeMetricsScrapingLogs = true
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrEmpty(lokiUrl) || !urlCheckRegex.IsMatch(lokiUrl)) throw new ArgumentException("Bad lokiUrl!");
|
||||
var labels = new List<string>();
|
||||
|
||||
if (additionalFiltratonLabels != null && additionalFiltratonLabels.Count > 0)
|
||||
{
|
||||
labels.AddRange(additionalFiltratonLabels);
|
||||
}
|
||||
labels.AddRange(DefaultLabels);
|
||||
hostBuilder.UseSerilog((ctx, lc) =>
|
||||
{
|
||||
if (excludeEFLogs)
|
||||
lc.Filter.ByExcluding(Matching.WithProperty("SourceContext", "Microsoft.EntityFrameworkCore.Database.Command"));
|
||||
if (excludeMetricsScrapingLogs)
|
||||
lc.Filter.ByExcluding(Matching.WithProperty("RequestPath", "/metrics"));
|
||||
lc.MinimumLevel.Is(minLevel);
|
||||
lc.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", EFMinLogLevel);
|
||||
lc.WriteTo.GrafanaLoki(lokiUrl,
|
||||
queueLimit: 1000000,
|
||||
labels: new List<LokiLabel>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Key = "service",
|
||||
Value = serviceName ?? AppDomain.CurrentDomain.FriendlyName
|
||||
}
|
||||
},
|
||||
restrictedToMinimumLevel: minLevel,
|
||||
propertiesAsLabels: labels,
|
||||
textFormatter: new LokiJsonTextFormatter()
|
||||
|
||||
);
|
||||
|
||||
if (writeToConsole)
|
||||
lc.WriteTo.Console();
|
||||
});
|
||||
|
||||
return hostBuilder;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KLHZ.Trader.Core\KLHZ.Trader.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,66 @@
|
|||
using KLHZ.Trader.Core.Common;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Contracts;
|
||||
using KLHZ.Trader.Core.Common.Messaging.Services;
|
||||
using KLHZ.Trader.Core.DataLayer;
|
||||
using KLHZ.Trader.Core.Declisions.Services;
|
||||
using KLHZ.Trader.Core.Exchange;
|
||||
using KLHZ.Trader.Core.Exchange.Services;
|
||||
using KLHZ.Trader.Core.TG;
|
||||
using KLHZ.Trader.Core.TG.Services;
|
||||
using KLHZ.Trader.Service.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using Serilog;
|
||||
using Telegram.Bot.Polling;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddControllers();
|
||||
|
||||
|
||||
builder.Services.AddMetrics();
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddSerilog();
|
||||
builder.Host.ConfigureSerilog(
|
||||
builder.Configuration.GetSection("LokiUrl").Value,
|
||||
serviceName: "klhz.trader",
|
||||
excludeEFLogs: false,
|
||||
excludeMetricsScrapingLogs: true,
|
||||
EFMinLogLevel: Serilog.Events.LogEventLevel.Warning
|
||||
);
|
||||
|
||||
builder.Services.AddInvestApiClient((_, settings) =>
|
||||
{
|
||||
settings.AccessToken = builder.Configuration.GetSection(nameof(ExchangeConfig))["Token"];
|
||||
});
|
||||
|
||||
builder.Services.AddDbContextFactory<TraderDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("PostgresConnection"));
|
||||
});
|
||||
|
||||
builder.Services.AddHostedService<BotStarter>();
|
||||
builder.Services.AddHostedService<ExchangeDataReader>();
|
||||
builder.Services.AddHostedService<Trader>();
|
||||
|
||||
builder.Services.AddSingleton<IUpdateHandler, BotMessagesHandler>();
|
||||
builder.Services.AddSingleton<BotModeSwitcher>();
|
||||
builder.Services.AddSingleton<IDataBus, DataBus>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
builder.Services.AddKeyedSingleton<ManagedAccount>(i);
|
||||
}
|
||||
|
||||
builder.Services.Configure<TgBotConfig>(builder.Configuration.GetSection(nameof(TgBotConfig)));
|
||||
builder.Services.Configure<ExchangeConfig>(builder.Configuration.GetSection(nameof(ExchangeConfig)));
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.MapMetrics();
|
||||
app.MapControllers();
|
||||
app.Run();
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"TgBotConfig": {
|
||||
"Token": "",
|
||||
"Admins": [ 227272610 ]
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PostgresConnection": ""
|
||||
},
|
||||
"LokiUrl": "",
|
||||
"ExchangeConfig": {
|
||||
"Token": "",
|
||||
"ManagingAccountNamePatterns": [ "автотрейд 1" ],
|
||||
"AllowedInstrumentsFigis": [ "BBG004730N88", "FUTIMOEXF000" ],
|
||||
"FutureComission": 0.0025,
|
||||
"ShareComission": 0.0004,
|
||||
"AccountCashPart": 0.05,
|
||||
"AccountCashPartFutures": 0.5,
|
||||
"DefaultBuyPartOfAccount": 0.3333
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36301.6
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KLHZ.Trader.Core", "KLHZ.Trader.Core\KLHZ.Trader.Core.csproj", "{D0978553-9459-48E7-B090-BD553C55E799}"
|
||||
EndProject
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{81DDED9D-158B-E303-5F62-77A2896D2A5A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KLHZ.Trader.Core.Tests", "KLHZ.Trader.Core.Tests\KLHZ.Trader.Core.Tests.csproj", "{A8FFAD9B-0CC8-4A44-95FC-7AB3068226DE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KLHZ.Trader.Service", "KLHZ.Trader.Service\KLHZ.Trader.Service.csproj", "{8AB0053E-6F6D-4AC6-A908-E0F404FF69C5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "prometheus", "prometheus", "{03E36D37-202B-4F4F-9EF3-09CF10BC1056}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
KLHZ.Trader.Infrastructure\prometheus\prometheus.yml = KLHZ.Trader.Infrastructure\prometheus\prometheus.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphana", "graphana", "{4A2C2DE7-39A9-43D2-AC17-0F0AE2E10276}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
KLHZ.Trader.Infrastructure\graphana\dashboard.json = KLHZ.Trader.Infrastructure\graphana\dashboard.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postrgres", "postrgres", "{174A800A-6040-40CF-B331-8603E097CBAC}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
KLHZ.Trader.Infrastructure\postgres\init.sql = KLHZ.Trader.Infrastructure\postgres\init.sql
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "loki", "loki", "{63D21DAF-FDF0-4F2D-A671-E9E59BB0CA5B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
KLHZ.Trader.Infrastructure\loki\loki-config.yaml = KLHZ.Trader.Infrastructure\loki\loki-config.yaml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KLHZ.Trader.HistoryLoader", "KLHZ.Trader.HistoryLoader\KLHZ.Trader.HistoryLoader.csproj", "{9BF1E4ED-CCD5-401B-9F1C-3B7625258F7E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D0978553-9459-48E7-B090-BD553C55E799}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D0978553-9459-48E7-B090-BD553C55E799}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D0978553-9459-48E7-B090-BD553C55E799}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D0978553-9459-48E7-B090-BD553C55E799}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8FFAD9B-0CC8-4A44-95FC-7AB3068226DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8FFAD9B-0CC8-4A44-95FC-7AB3068226DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8FFAD9B-0CC8-4A44-95FC-7AB3068226DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8FFAD9B-0CC8-4A44-95FC-7AB3068226DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8AB0053E-6F6D-4AC6-A908-E0F404FF69C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8AB0053E-6F6D-4AC6-A908-E0F404FF69C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8AB0053E-6F6D-4AC6-A908-E0F404FF69C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8AB0053E-6F6D-4AC6-A908-E0F404FF69C5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9BF1E4ED-CCD5-401B-9F1C-3B7625258F7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9BF1E4ED-CCD5-401B-9F1C-3B7625258F7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9BF1E4ED-CCD5-401B-9F1C-3B7625258F7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9BF1E4ED-CCD5-401B-9F1C-3B7625258F7E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{03E36D37-202B-4F4F-9EF3-09CF10BC1056} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}
|
||||
{4A2C2DE7-39A9-43D2-AC17-0F0AE2E10276} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}
|
||||
{174A800A-6040-40CF-B331-8603E097CBAC} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}
|
||||
{63D21DAF-FDF0-4F2D-A671-E9E59BB0CA5B} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {25E56E6E-B6FE-4B25-BDAA-CC88076B23A4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,15 @@
|
|||
services:
|
||||
klhz.trader.service:
|
||||
restart: always
|
||||
image: klhz_trader_service
|
||||
hostname: klhz_trader
|
||||
ports:
|
||||
- 8080:8080
|
||||
build:
|
||||
context: .
|
||||
dockerfile: KLHZ.Trader.Service/Dockerfile
|
||||
environment:
|
||||
LokiUrl: "loki:3100"
|
||||
TgBotConfig__Token: "${TG_BOT_TOKEN}"
|
||||
ExchangeConfig__Token: "${EXCHANGE_API_TOKEN}"
|
||||
ConnectionStrings__PostgresConnection: "${PG_CONNECTION_STRING}"
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectVersion>2.1</ProjectVersion>
|
||||
<DockerTargetOS>Linux</DockerTargetOS>
|
||||
<DockerPublishLocally>False</DockerPublishLocally>
|
||||
<ProjectGuid>81dded9d-158b-e303-5f62-77a2896d2a5a</ProjectGuid>
|
||||
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
|
||||
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/swagger</DockerServiceUrl>
|
||||
<DockerServiceName>klhz.trader.api</DockerServiceName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include=".env" />
|
||||
<None Include="docker-compose.override.yml">
|
||||
<DependentUpon>docker-compose.yml</DependentUpon>
|
||||
</None>
|
||||
<None Include="docker-compose.yml" />
|
||||
<None Include=".dockerignore" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,90 @@
|
|||
services:
|
||||
klhz.trader.service:
|
||||
restart: always
|
||||
image: klhz_trader_service
|
||||
hostname: klhz_trader
|
||||
ports:
|
||||
- 8080:8080
|
||||
build:
|
||||
context: .
|
||||
dockerfile: KLHZ.Trader.Service/Dockerfile
|
||||
environment:
|
||||
LokiUrl: "http://loki:3100"
|
||||
TgBotConfig__Token: "${TG_BOT_TOKEN}"
|
||||
ExchangeConfig__Token: "${EXCHANGE_API_TOKEN}"
|
||||
ConnectionStrings__PostgresConnection: "${PG_CONNECTION_STRING}"
|
||||
|
||||
postgresql:
|
||||
ports:
|
||||
- 15433:5432
|
||||
container_name: debug_postgresql_16
|
||||
hostname: debug_postgresql_16
|
||||
image: postgres:16
|
||||
restart: always
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
- "max_connections=100"
|
||||
- "-c"
|
||||
- "shared_buffers=512MB"
|
||||
- "-c"
|
||||
- "temp_buffers=64MB"
|
||||
- "-c"
|
||||
- "log_statement=all"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: QW12cv9001
|
||||
POSTGRES_DB: trading
|
||||
volumes:
|
||||
- traderdata:/var/lib/postgresql/data
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
container_name: prometheus
|
||||
ports:
|
||||
- 9191:9090
|
||||
restart: always
|
||||
volumes:
|
||||
- ./KLHZ.Trader.Infrastructure/prometheus/:/etc/prometheus/
|
||||
- prom_data:/prometheus
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
container_name: grafana
|
||||
ports:
|
||||
- 1300:3000
|
||||
restart: always
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_USER: "${GF_SECURITY_ADMIN_USER}"
|
||||
GF_SECURITY_ADMIN_PASSWORD: "${GF_SECURITY_ADMIN_PASSWORD}"
|
||||
volumes:
|
||||
- graphana:/etc/grafana/provisioning/datasources
|
||||
|
||||
loki:
|
||||
hostname: loki
|
||||
image: grafana/loki:latest
|
||||
container_name: loki
|
||||
ports:
|
||||
- "2300:3100"
|
||||
volumes:
|
||||
- ./KLHZ.Trader.Infrastructure/loki/loki-config.yaml:/etc/loki/local-config.yaml
|
||||
- loki_data:/loki
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
|
||||
klhz.trader.historyloader:
|
||||
image: klhztraderhistoryloader
|
||||
ports:
|
||||
- "5004:8080"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: KLHZ.Trader.HistoryLoader/Dockerfile
|
||||
environment:
|
||||
ExchangeConfig__Token: "${EXCHANGE_API_TOKEN}"
|
||||
ConnectionStrings__PostgresConnection: "${PG_CONNECTION_STRING}"
|
||||
|
||||
volumes:
|
||||
traderdata:
|
||||
prom_data:
|
||||
graphana:
|
||||
loki_data:
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"profiles": {
|
||||
"Docker Compose": {
|
||||
"commandName": "DockerCompose",
|
||||
"commandVersion": "1.0",
|
||||
"launchBrowser": false,
|
||||
"serviceActions": {
|
||||
"klhz.trader.service": "StartDebugging",
|
||||
"klhz.trader.historyloader": "StartDebugging"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue