Positionsverwaltung: Herkunft sichtbar, Zuordnung aenderbar, Sperren einstellbar
Erster Teil von #485. Der PositionManager im Core beantwortet drei Fragen, die sich beim Debuggen sofort stellen: Wem gehoert eine Position, was sagt der Broker dazu, und was ist gesperrt. Drei Zugaenge auf denselben Dienst - das Fenster der Shell, --positions und --reassign im kopflosen Betrieb, GET /api/positions. Der Testbetrieb laeuft als systemd-Dienst; ein Werkzeug, das nur in der Oberflaeche existiert, fehlt genau dann, wenn man es braucht. Entscheidungen, die im Code begruendet stehen: - BrokerQuantity ist nullable und heisst "nicht bekannt", nicht "null Stueck". Ohne Verbindung sieht ein leergeraeumtes Konto genauso aus wie eine Stoerung; aus dem einen darf nicht auf das andere geschlossen werden. - Umbuchen aendert die Zuordnung, nie die Menge. Eine Umbuchung, die eine Stueckzahl bewegt, waere ein Handel ohne Order und stuende in keiner Historie. - Gegen eine entgegengesetzte Position wird abgelehnt: Das waere eine Glattstellung und gehoert ueber eine Order gemacht, mit Preis und Historie. - Das Quellbuch ist Pflicht - Schutz gegen die falsche Zeile -, ebenso eine Begruendung, sonst ist der Eingriff spaeter nicht mehr erklaerbar. - Das Cap des Zielbuchs sperrt NICHT. Die Position existiert bereits; sie zu benennen erzeugt kein neues Risiko. Wuerde die Grenze das verhindern, bliebe jede zu grosse Position fuer immer in UNASSIGNED. - Der REST-Endpunkt ist lesend. Der Listener kennt keine Anmeldung; ein schreibender Zugriff waere ein Eingriff in die Buecher, den niemand belegen kann. Beim ersten Testlauf gefunden: (Module, ContractKey) ist der Primaerschluessel von core_position, das Buch laesst sich in der Zeile nicht ueberschreiben. Umgebucht wird deshalb durch Loeschen und Neuanlegen. 471 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
using System.Globalization;
|
||||
using IBKRTrader.Core.Capital;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using IBKRTrader.Core.Persistence.Entities;
|
||||
using IBKRTrader.Core.Trading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IBKRTrader.Core.Positions;
|
||||
|
||||
/// <summary>
|
||||
/// Eine Position, wie die Verwaltung sie zeigt: eigener Bestand, Herkunft, Sperren – und daneben,
|
||||
/// was der Broker führt.
|
||||
/// </summary>
|
||||
/// <param name="Book">Buch, dem sie zugeordnet ist. MANUAL und UNASSIGNED sind gleichwertige Bücher.</param>
|
||||
/// <param name="Instrument">Betroffenes Instrument.</param>
|
||||
/// <param name="Quantity">Eigener Bestand, vorzeichenbehaftet.</param>
|
||||
/// <param name="AvgPrice">Durchschnittlicher Einstand.</param>
|
||||
/// <param name="BoundCapital">Gebundenes Kapital – bei geschriebenen Puts die Verpflichtung, nicht die Prämie.</param>
|
||||
/// <param name="Lock">Sperrstufe des Betreibers für dieses Buch und Instrument.</param>
|
||||
/// <param name="Rule">Regel des Instruments (Handelsverbot, Stückgrenze) – gilt über alle Bücher.</param>
|
||||
/// <param name="BrokerQuantity">
|
||||
/// Was der Broker führt. <c>null</c> heißt <b>nicht bekannt</b> und ausdrücklich nicht „null Stück":
|
||||
/// Ohne Verbindung ist jede Aussage über Abweichungen wertlos.
|
||||
///
|
||||
/// <para>Dezimal und nicht ganzzahlig, weil der Broker es so meldet – Bruchstücke gibt es. Sie hier
|
||||
/// zu runden hieße, eine Abweichung wegzurechnen, die es gibt.</para>
|
||||
/// </param>
|
||||
public sealed record ManagedPosition(
|
||||
BookId Book,
|
||||
InstrumentRef Instrument,
|
||||
int Quantity,
|
||||
decimal AvgPrice,
|
||||
decimal BoundCapital,
|
||||
PositionLock Lock,
|
||||
InstrumentRule Rule,
|
||||
decimal? BrokerQuantity)
|
||||
{
|
||||
/// <summary>Herkunft in einem Wort – das, wonach beim Aufräumen zuerst gefragt wird.</summary>
|
||||
public string Origin => Book.Kind switch
|
||||
{
|
||||
BookKind.Manual => "Von Hand",
|
||||
BookKind.Unassigned => "Herkunft unbekannt",
|
||||
BookKind.House => "Haus",
|
||||
_ => Book.Value
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Weicht unser Bestand von dem des Brokers ab? <c>false</c>, solange der Broker nichts gesagt
|
||||
/// hat – eine Abweichung, die nur aus fehlenden Daten besteht, ist keine.
|
||||
/// </summary>
|
||||
public bool Diverges => BrokerQuantity is { } broker && broker != Quantity;
|
||||
|
||||
public bool IsRestricted => Lock != PositionLock.Free || Rule.IsRestrictive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Was der Überblick über alle Bücher hergibt.
|
||||
/// </summary>
|
||||
/// <param name="Positions">Alle offenen Positionen, über sämtliche Bücher.</param>
|
||||
/// <param name="BrokerReachable">
|
||||
/// Stand die Verbindung zum Broker? Ist sie es nicht, sind sämtliche <c>BrokerQuantity</c> leer und
|
||||
/// aus dem Vergleich darf nichts gefolgert werden.
|
||||
/// </param>
|
||||
/// <param name="UnknownToUs">
|
||||
/// Positionen, die der Broker führt und wir in keinem Buch haben. Sie sind der eigentliche Grund
|
||||
/// für diese Ansicht: Bestand, den niemand angelegt zu haben behauptet.
|
||||
/// </param>
|
||||
public sealed record PositionOverview(
|
||||
IReadOnlyList<ManagedPosition> Positions,
|
||||
bool BrokerReachable,
|
||||
IReadOnlyList<BrokerPosition> UnknownToUs);
|
||||
|
||||
/// <summary>
|
||||
/// Die Positionsverwaltung (#485): sehen, wem eine Position gehört, sie umhängen und sie
|
||||
/// stufenweise sperren.
|
||||
///
|
||||
/// <para><b>Warum das ein eigener Dienst ist und nicht Teil der Oberfläche.</b> Alle drei
|
||||
/// Handlungen sind Eingriffe des Betreibers in die Buchführung, nicht Anzeige. Sie brauchen
|
||||
/// dieselbe Sorgfalt wie ein Handelspfad: eine Umbuchung, die eine Menge verändert, wäre ein
|
||||
/// stiller Handel; eine Sperre, die nur in einem Fenster steht, wäre keine. Deshalb liegen sie
|
||||
/// hier – abrufbar aus der Shell, aus der REST-Schnittstelle und aus dem Daemon, mit identischem
|
||||
/// Verhalten.</para>
|
||||
///
|
||||
/// <para><b>Was diese Klasse nicht tut:</b> Sie handelt nicht. Kein Aufruf hier löst eine Order
|
||||
/// aus, weder zum Glattstellen noch zum Ausgleichen einer Abweichung. Was mit einer fremden
|
||||
/// Position geschieht, entscheidet ein Mensch.</para>
|
||||
/// </summary>
|
||||
public sealed class PositionManager
|
||||
{
|
||||
private const string LogModule = "Positionen";
|
||||
|
||||
private readonly IDbContextFactory<CoreDbContext> _dbf;
|
||||
private readonly IPortfolioService _portfolio;
|
||||
private readonly IBrokerPortfolioReader _broker;
|
||||
private readonly TradingRuleService _rules;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
public PositionManager(
|
||||
IDbContextFactory<CoreDbContext> dbf,
|
||||
IPortfolioService portfolio,
|
||||
IBrokerPortfolioReader broker,
|
||||
TradingRuleService rules,
|
||||
LoggingService logger)
|
||||
{
|
||||
_dbf = dbf;
|
||||
_portfolio = portfolio;
|
||||
_broker = broker;
|
||||
_rules = rules;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// ─── Sehen ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Der Überblick über alle Bücher, angereichert um Sperren und den Bestand des Brokers.
|
||||
/// </summary>
|
||||
/// <param name="includeBroker">
|
||||
/// Den Broker mit abfragen. <c>false</c> für eine reine Anzeige der eigenen Buchführung – die
|
||||
/// Abfrage kostet eine Verbindung und mehrere Sekunden.
|
||||
/// </param>
|
||||
public async Task<PositionOverview> GetOverviewAsync(
|
||||
bool includeBroker = true, CancellationToken ct = default)
|
||||
{
|
||||
var own = await _portfolio.GetAllPositionsAsync(ct).ConfigureAwait(false);
|
||||
|
||||
IReadOnlyList<BrokerPosition> brokerPositions = Array.Empty<BrokerPosition>();
|
||||
var reachable = false;
|
||||
|
||||
if (includeBroker)
|
||||
{
|
||||
brokerPositions = await _broker.GetPositionsAsync(ct).ConfigureAwait(false);
|
||||
|
||||
// Eine leere Antwort ist zweideutig: keine Positionen oder keine Verbindung. Solange wir
|
||||
// das nicht auseinanderhalten koennen, gilt sie NICHT als Feststellung - sonst sieht ein
|
||||
// Verbindungsabbruch aus wie ein leergeraeumtes Konto.
|
||||
reachable = brokerPositions.Count > 0;
|
||||
}
|
||||
|
||||
var byKey = brokerPositions
|
||||
.Where(p => p.Instrument is not null)
|
||||
.GroupBy(p => p.Instrument!.Value.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(p => p.Quantity), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var managed = new List<ManagedPosition>(own.Count);
|
||||
foreach (var position in own.Where(p => p.Quantity != 0))
|
||||
{
|
||||
var book = BookId.Parse(position.Module);
|
||||
|
||||
// Verbindung steht und der Broker kennt das Papier nicht: Das ist eine Aussage – nämlich
|
||||
// null Stück – und keine fehlende Angabe. Ohne Verbindung bleibt es leer, weil dann
|
||||
// jeder Vergleich wertlos wäre.
|
||||
decimal? brokerQuantity = null;
|
||||
if (reachable)
|
||||
brokerQuantity = byKey.TryGetValue(position.Instrument.Key, out var q) ? q : 0m;
|
||||
|
||||
managed.Add(new ManagedPosition(
|
||||
Book: book,
|
||||
Instrument: position.Instrument,
|
||||
Quantity: position.Quantity,
|
||||
AvgPrice: position.AvgPrice,
|
||||
BoundCapital: position.BoundCapital,
|
||||
Lock: await _rules.GetLockAsync(book, position.Instrument, ct).ConfigureAwait(false),
|
||||
Rule: await _rules.GetRuleAsync(position.Instrument, ct).ConfigureAwait(false),
|
||||
BrokerQuantity: brokerQuantity));
|
||||
}
|
||||
|
||||
// Bestand, den der Broker fuehrt und den wir in keinem Buch haben. Der Abgleich (#306)
|
||||
// schreibt solche Positionen nach UNASSIGNED; bis er gelaufen ist, stehen sie hier.
|
||||
var ownKeys = own.Where(p => p.Quantity != 0)
|
||||
.Select(p => p.Instrument.Key)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var unknown = brokerPositions
|
||||
.Where(p => p.Quantity != 0)
|
||||
.Where(p => p.Instrument is not { } key || !ownKeys.Contains(key.Key))
|
||||
.ToList();
|
||||
|
||||
return new PositionOverview(
|
||||
[.. managed.OrderBy(m => m.Book.Value, StringComparer.Ordinal)
|
||||
.ThenBy(m => m.Instrument.Key, StringComparer.Ordinal)],
|
||||
reachable,
|
||||
unknown);
|
||||
}
|
||||
|
||||
// ─── Umhängen ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Ergebnis einer Umbuchung.</summary>
|
||||
/// <param name="Success">Hat sie stattgefunden?</param>
|
||||
/// <param name="Message">Was geschehen ist bzw. warum nicht.</param>
|
||||
public readonly record struct ReassignResult(bool Success, string Message);
|
||||
|
||||
/// <summary>
|
||||
/// Hängt eine Position von einem Buch in ein anderes um – der häufigste Fall ist UNASSIGNED
|
||||
/// nach MANUAL, wenn der Betreiber eine Position wiedererkennt.
|
||||
///
|
||||
/// <para><b>Die Menge bleibt unangetastet.</b> Umgehängt wird die Zuordnung, nicht der Bestand.
|
||||
/// Eine Umbuchung, die eine Stückzahl verändert, wäre ein Handel ohne Order – und stünde in
|
||||
/// keiner Historie.</para>
|
||||
///
|
||||
/// <para><b>Warum das Cap des Zielbuchs hier nicht sperrt.</b> Die Position existiert bereits;
|
||||
/// sie zu benennen erzeugt kein neues Risiko. Würde die Grenze die Umbuchung verhindern, bliebe
|
||||
/// jede zu große Position für immer in UNASSIGNED – dem einzigen Buch ohne Grenze. Das
|
||||
/// Überschreiten ist ein zulässiger Zustand (<see cref="Headroom.OverCap"/>): Es sperrt Zukäufe,
|
||||
/// heilt aber nichts durch Verkauf. Gemeldet wird es trotzdem, und zwar deutlich.</para>
|
||||
/// </summary>
|
||||
/// <param name="instrument">Die betroffene Position.</param>
|
||||
/// <param name="from">Buch, aus dem sie kommt – als Schutz gegen das Umhängen der falschen Zeile.</param>
|
||||
/// <param name="to">Zielbuch.</param>
|
||||
/// <param name="reason">Begründung des Betreibers; landet im Log.</param>
|
||||
public async Task<ReassignResult> ReassignAsync(
|
||||
InstrumentRef instrument, BookId from, BookId to, string reason,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (from == to)
|
||||
return new ReassignResult(false, $"Quelle und Ziel sind dasselbe Buch ({from.Value}).");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
return new ReassignResult(false,
|
||||
"Eine Umbuchung ohne Begründung ist später nicht mehr nachvollziehbar.");
|
||||
|
||||
await using var db = await _dbf.CreateDbContextAsync(ct).ConfigureAwait(false);
|
||||
|
||||
var source = await db.Positions
|
||||
.FirstOrDefaultAsync(p => p.Module == from.Value && p.ContractKey == instrument.Key, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (source is null || source.Quantity == 0)
|
||||
return new ReassignResult(false,
|
||||
$"Im Buch {from.Value} liegt keine offene Position auf {instrument.Key}.");
|
||||
|
||||
var target = await db.Positions
|
||||
.FirstOrDefaultAsync(p => p.Module == to.Value && p.ContractKey == instrument.Key, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var moved = source.Quantity;
|
||||
var movedCost = source.AvgPrice;
|
||||
|
||||
// Die Gegenrichtung zuerst, BEVOR irgendetwas angefasst wird: 100 Stück long in ein Buch zu
|
||||
// schieben, das 100 short hält, ergibt eine Position von null - also eine Glattstellung. Die
|
||||
// gehört über eine Order gemacht, mit Preis und Historie, nicht über die Verwaltung.
|
||||
if (target is not null && target.Quantity != 0 &&
|
||||
Math.Sign(target.Quantity) != Math.Sign(source.Quantity))
|
||||
return new ReassignResult(false,
|
||||
$"{to.Value} hält {instrument.Key} in der Gegenrichtung " +
|
||||
$"({target.Quantity} gegen {source.Quantity}). Das ist eine Glattstellung, keine " +
|
||||
"Umbuchung – sie gehört über eine Order gemacht, nicht über die Verwaltung.");
|
||||
|
||||
// Immer löschen und neu anlegen, nie das Buch in der bestehenden Zeile überschreiben:
|
||||
// (Module, ContractKey) ist der Primärschlüssel von core_position. EF lehnt eine Änderung am
|
||||
// Schlüssel ab - im Test sofort, in Betrieb ebenso. Aufgefallen beim ersten Testlauf.
|
||||
var quantity = (target?.Quantity ?? 0) + source.Quantity;
|
||||
var price = target is null || quantity == 0
|
||||
? source.AvgPrice
|
||||
// Zusammengelegt wird mengengewichtet. Ein neuer Einstand wäre eine erfundene Zahl, und
|
||||
// die GuV des Zielbuchs hängt daran.
|
||||
: (target.Quantity * target.AvgPrice + source.Quantity * source.AvgPrice) / quantity;
|
||||
|
||||
var moving = new CorePosition
|
||||
{
|
||||
Module = to.Value,
|
||||
ContractKey = source.ContractKey,
|
||||
Symbol = source.Symbol,
|
||||
Kind = source.Kind,
|
||||
Expiry = source.Expiry,
|
||||
Strike = source.Strike,
|
||||
Right = source.Right,
|
||||
Multiplier = source.Multiplier,
|
||||
Currency = source.Currency,
|
||||
Quantity = quantity,
|
||||
AvgPrice = price,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
db.Positions.Remove(source);
|
||||
if (target is not null) db.Positions.Remove(target);
|
||||
await db.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||
|
||||
db.Positions.Add(moving);
|
||||
await db.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||
|
||||
var text = string.Create(CultureInfo.InvariantCulture,
|
||||
$"Position umgebucht: {moved} x {instrument.Key} zu {movedCost:F2} von {from.Value} " +
|
||||
$"nach {to.Value}. Grund: {reason}");
|
||||
|
||||
// Warn und nicht Info: Das ist ein Eingriff von Hand in die Buchfuehrung. Er soll in der
|
||||
// Datei stehen, die man liest, wenn man wissen will, warum eine Zahl sich geaendert hat.
|
||||
_logger.Warn(LogModule, text);
|
||||
|
||||
return new ReassignResult(true, text);
|
||||
}
|
||||
|
||||
// ─── Sperren ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Setzt die Sperrstufe einer Position. Reicht durch an den <see cref="TradingRuleService"/>,
|
||||
/// damit die Oberfläche nur einen Dienst kennen muss.
|
||||
/// </summary>
|
||||
public Task SetLockAsync(
|
||||
BookId book, InstrumentRef instrument, PositionLock level, string? note = null,
|
||||
CancellationToken ct = default) =>
|
||||
_rules.SetLockAsync(book, instrument, level, note, ct);
|
||||
|
||||
/// <summary>Setzt oder löscht die Regel eines Instruments (Handelsverbot, Stückgrenze).</summary>
|
||||
public Task SetRuleAsync(
|
||||
string contractKeyOrSymbol, InstrumentRule rule, string? note = null,
|
||||
CancellationToken ct = default) =>
|
||||
_rules.SetRuleAsync(contractKeyOrSymbol, rule, note, ct);
|
||||
}
|
||||
@@ -4,18 +4,26 @@ using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Positions;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
namespace IBKRTrader.Core.Workers.BuiltIn;
|
||||
|
||||
/// <summary>
|
||||
/// WebApiService – permanenter REST-API-Service auf Basis von HttpListener.
|
||||
/// Basis-Routen: GET /api/status, GET /api/workers
|
||||
/// Wird später schrittweise um vollständige API-Endpunkte erweitert.
|
||||
///
|
||||
/// <para>Routen: <c>GET /api/status</c>, <c>GET /api/workers</c>, <c>GET /api/positions</c>.</para>
|
||||
///
|
||||
/// <para><b>Ausschließlich lesend, und das ist Absicht.</b> Der Listener lauscht auf
|
||||
/// <c>localhost</c> und kennt keine Anmeldung. Solange das so ist, gehört hierher nichts, was
|
||||
/// etwas verändert – eine Umbuchung oder eine Sperre über eine offene Schnittstelle wäre ein
|
||||
/// Eingriff in die Bücher, den niemand belegen kann. Schreibende Endpunkte kommen erst mit
|
||||
/// Anmeldung und Herkunftsnachweis.</para>
|
||||
/// </summary>
|
||||
public class WebApiService : WorkerBase
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
private readonly PositionManager _positions;
|
||||
private WorkerEngine? _engine; // wird nach DI-Aufbau gesetzt (kein readonly)
|
||||
private HttpListener? _listener;
|
||||
|
||||
@@ -28,10 +36,12 @@ public class WebApiService : WorkerBase
|
||||
|
||||
protected override TimeSpan? Interval => null;
|
||||
|
||||
public WebApiService(LoggingService logger, IDbContextFactory<CoreDbContext> db, SettingsService settings)
|
||||
public WebApiService(LoggingService logger, IDbContextFactory<CoreDbContext> db,
|
||||
SettingsService settings, PositionManager positions)
|
||||
: base(logger, db)
|
||||
{
|
||||
_settings = settings;
|
||||
_settings = settings;
|
||||
_positions = positions;
|
||||
Info.Active = settings.Settings.Webserver.Enabled;
|
||||
Info.RunEvery = "Service";
|
||||
}
|
||||
@@ -69,6 +79,50 @@ public class WebApiService : WorkerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der Positionsbestand über alle Bücher (#485).
|
||||
///
|
||||
/// <para>Die Broker-Abfrage kostet eine Verbindung und mehrere Sekunden und bleibt deshalb
|
||||
/// abschaltbar: <c>?broker=0</c> antwortet allein aus der eigenen Buchführung. <c>brokerQuantity</c>
|
||||
/// ist <c>null</c>, wenn der Broker nichts gemeldet hat – das ist ausdrücklich etwas anderes als
|
||||
/// null Stück, und ein Client darf daraus keine Abweichung ableiten.</para>
|
||||
/// </summary>
|
||||
private async Task<object> PositionsAsync(HttpListenerContext ctx)
|
||||
{
|
||||
var includeBroker = !string.Equals(
|
||||
ctx.Request.QueryString["broker"], "0", StringComparison.Ordinal);
|
||||
|
||||
var overview = await _positions.GetOverviewAsync(includeBroker).ConfigureAwait(false);
|
||||
|
||||
return new
|
||||
{
|
||||
brokerReachable = overview.BrokerReachable,
|
||||
positions = overview.Positions.Select(p => new
|
||||
{
|
||||
book = p.Book.Value,
|
||||
origin = p.Origin,
|
||||
instrument = p.Instrument.Key,
|
||||
currency = p.Instrument.Currency,
|
||||
quantity = p.Quantity,
|
||||
avgPrice = p.AvgPrice,
|
||||
boundCapital = p.BoundCapital,
|
||||
brokerQuantity = p.BrokerQuantity,
|
||||
diverges = p.Diverges,
|
||||
positionLock = p.Lock.ToString(),
|
||||
blocked = p.Rule.Blocked,
|
||||
maxQuantity = p.Rule.MaxQuantity
|
||||
}),
|
||||
unknownToUs = overview.UnknownToUs.Select(p => new
|
||||
{
|
||||
symbol = p.Symbol,
|
||||
secType = p.SecType,
|
||||
currency = p.Currency,
|
||||
quantity = p.Quantity,
|
||||
avgCost = p.AvgCost
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private async Task RouteAsync(HttpListenerContext ctx)
|
||||
{
|
||||
try
|
||||
@@ -81,6 +135,7 @@ public class WebApiService : WorkerBase
|
||||
"/api/workers" => _engine?.WorkerInfos
|
||||
.Select(w => new { w.WorkerName, w.Module, w.Type,
|
||||
w.Info, w.Active }) ?? [],
|
||||
"/api/positions" => await PositionsAsync(ctx).ConfigureAwait(false),
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user