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