Fix category mapping and TotalTrades drift, plan traits/tiering (Teil D)
- MarketCategoryMapper: classify from question text (the Gamma /markets
endpoint delivers neither category nor event tags, so on-demand markets
had no signal at all), match short tokens on word boundaries ("eth" no
longer hits inside "whether", "pop" not inside "popular"), widen the
keyword lists across all categories.
- UpdateMarketFields: never overwrite a tag-derived category with an
uninformative "Other" from the on-demand path.
- PositionPnLEngine: sync Trader.TotalTrades to the actual replayed row
count — the worker-side increment counters drift (INSERT IGNORE,
deletions, historic imports) and produced Trades30d > TotalTrades.
- Tests: 14 new (mapper classification + word-boundary regression,
TotalTrades sync + Trades30d invariant, category update guard via
SQLite) — suite now 32 green + 1 skip.
- FIXPLAN Teil D for the larger rebuilds (AggregatedCount column,
TraderTraits heuristics, IngestMode tiering for ultra-HF traders).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
940a99fec2
commit
d2f3ec2bd0
@@ -318,3 +318,93 @@ zukünftiges Pruning ist damit verlustfrei im Sinne der PnL-Summen.
|
|||||||
7. Watchlist: Toggle auf der Detailseite wechselt sichtbar den Zustand; die neue Watchlist-Seite listet die
|
7. Watchlist: Toggle auf der Detailseite wechselt sichtbar den Zustand; die neue Watchlist-Seite listet die
|
||||||
beobachteten Trader; Remove funktioniert. *(A10)*
|
beobachteten Trader; Remove funktioniert. *(A10)*
|
||||||
8. „Run Deep Analysis" (KI) füllt die AI Strategy Analysis auf der Detailseite tatsächlich. *(A10)*
|
8. „Run Deep Analysis" (KI) füllt die AI Strategy Analysis auf der Detailseite tatsächlich. *(A10)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Teil D — Ausbaustufe: Merkmals-Tags & HF-Trader-Tiering (ergänzt 2026-07-11)
|
||||||
|
|
||||||
|
> **Bereits direkt erledigt (nicht Teil dieses Auftrags):** `TotalTrades` wird jetzt von der Engine aus dem
|
||||||
|
> echten Row-Count gesetzt; der Kategorie-Mapper klassifiziert zusätzlich über den Frage-Text und matcht kurze
|
||||||
|
> Tokens nur an Wortgrenzen; `UpdateMarketFields` überschreibt gute Kategorien nicht mehr mit "Other".
|
||||||
|
> Teststand: **32 grün + 1 Skip** — das ist die neue Basis, Assertions unverändert lassen.
|
||||||
|
|
||||||
|
### D1. `AggregatedCount`-Spalte (Grundlage für D2/D3)
|
||||||
|
|
||||||
|
- Migration: `Trades.AggregatedCount INT NULL` (NULL = einzelner Roh-Trade).
|
||||||
|
- Kompaktierung im `TradeRetentionWorker`: schreibt `AggregatedCount = Anzahl der ersetzten Trades`
|
||||||
|
(heute geht die Original-Anzahl verloren!) und summiert beim erneuten Kompaktieren
|
||||||
|
bestehende Aggregate (`Sum(t.AggregatedCount ?? 1)`).
|
||||||
|
- Engine: `trader.TotalTrades = trades.Sum(t => t.AggregatedCount ?? 1)` (ersetzt `trades.Count`),
|
||||||
|
`analytics.Trades30d` analog.
|
||||||
|
- **Test:** Kompaktierung von 5 Trades → 1 Aggregat mit `AggregatedCount = 5`; `TotalTrades` bleibt nach
|
||||||
|
Recalc 5, nicht 1.
|
||||||
|
|
||||||
|
### D2. TraderTraits — heuristische Strategie-Merkmale (ohne KI)
|
||||||
|
|
||||||
|
- Neue Tabelle `TraderTraits`: Id, TraderId (FK, Cascade), Trait (string ≤ 64), Value (decimal, Messwert),
|
||||||
|
ComputedAt. Unique-Index (TraderId, Trait). Pro Analyse-Lauf upserten, nicht mehr zutreffende Traits löschen.
|
||||||
|
- Berechnung als **pure Funktion** `TraderTraitCalculator.Compute(trader, trades, positions)` →
|
||||||
|
Liste (Trait, Value); Aufruf im `TraderAnalyticsWorker` nach Engine + Estimator aus den **bereits geladenen**
|
||||||
|
Daten — keine zusätzlichen API-Calls.
|
||||||
|
- Traits v1 (Trait vergeben, wenn Bedingung erfüllt; Value = Messgröße):
|
||||||
|
|
||||||
|
| Trait | Regel |
|
||||||
|
|---|---|
|
||||||
|
| `sub_second_cadence` | Median-Intervall < 2 s bei ≥ 50 Trades (Value = Median in s) |
|
||||||
|
| `always_on_24_7` | größte Inaktivitätslücke der letzten 7 Tage < 4 h bei ≥ 200 Trades/7d |
|
||||||
|
| `uniform_sizes` | Variationskoeffizient der Size (letzte 200 Trades) < 0,1 |
|
||||||
|
| `round_amounts` | > 60 % der Amounts ∈ {1,5,10,20,25,50,100,250,500,1000} ± 1 % |
|
||||||
|
| `uses_split_merge` | Anteil Split+Merge > 10 % |
|
||||||
|
| `both_sides_same_market` | Yes- UND No-Trades in > 20 % der Märkte |
|
||||||
|
| `resolution_farming` | > 30 % der Buys mit Price ≥ 0,93 UND < 48 h vor `ClosedAt` (min. 10 Buys) |
|
||||||
|
| `longshot_buyer` | > 30 % der Buys mit Price ≤ 0,10 |
|
||||||
|
| `scalper` | mediane Haltedauer < 1 h |
|
||||||
|
| `holds_to_resolution` | > 70 % der aufgelösten Positionen ohne vorherigen Sell |
|
||||||
|
| `fresh_wallet` | erster Trade < 30 Tage (Value = Alter in Tagen) |
|
||||||
|
| `stable_stake_fraction` | CV von Amount/EstimatedBankroll < 0,5 (nur bei Bankroll > 0) |
|
||||||
|
|
||||||
|
- Schwellwerte als Konstanten im Calculator (v1 hart kodiert ist ok).
|
||||||
|
- API/UI: `TraderDto`/`TraderDetailDto` um `Traits` (string-Liste) erweitern; Detailseite zeigt Chips
|
||||||
|
unter dem Strategy-Feld; Traders-Liste bekommt einen Trait-Filter.
|
||||||
|
- KI-Integration: `AiStrategyAnalysisService`-Prompt bekommt die Merkmalsliste; Trade-Beispiele von 50 auf
|
||||||
|
15 repräsentative reduzieren (5 größte, 5 jüngste, 5 zufällige) — die KI verifiziert Hypothesen statt zu raten.
|
||||||
|
- **Tests:** pure-Function-Tests je Trait, mindestens Positiv- UND Negativfall für `resolution_farming`,
|
||||||
|
`sub_second_cadence`, `uniform_sizes`, `both_sides_same_market`.
|
||||||
|
|
||||||
|
### D3. Trader-Tiering (`IngestMode`) — Umgang mit Ultra-HF-Tradern (RN1, Swisstony)
|
||||||
|
|
||||||
|
**Hintergrund:** Ultra-HF-Trader werden heute schon NICHT vollständig erfasst (PollingWorker: 100 Trades/60 s
|
||||||
|
gegen 300+/min) — das Trade-Replay-PnL ist für diese Klasse bereits falsch und frisst nur Speicher.
|
||||||
|
|
||||||
|
- Enum `IngestMode { Full = 0, Aggregated = 1, SnapshotOnly = 2 }` + Spalte auf `Trader` (Default Full), Migration.
|
||||||
|
- **Klassifizierung** im `TradeHistoryWorker` nach jedem Fetch: Zeitspanne der letzten 500 Trades →
|
||||||
|
Trades/Tag-Schätzung. > 5.000/Tag → SnapshotOnly; > 100/Tag → Aggregated. Hysterese: Rückstufung Richtung
|
||||||
|
Full erst nach 7 Tagen unter der halben Schwelle (kein Flattern).
|
||||||
|
- **SnapshotOnly (Tier C):**
|
||||||
|
- Polling/History-Worker überspringen den Trade-Import komplett.
|
||||||
|
- Stündlich: `GetTraderPositionsAsync` (der ungenutzte `/positions`-Endpoint!) → `TraderPositions` upserten
|
||||||
|
(size→SharesHeld, avgPrice→AvgCost, cashPnl→RealizedPnl); `OverallPnL` aus Positions +
|
||||||
|
`GetLeaderboardAsync`-PnL für die Zeitfenster; `TraderDailySnapshot` weiter schreiben (Equity-Kurve bleibt).
|
||||||
|
- Wöchentliche „Biopsie": einmal 500 Trades via /activity ziehen, NUR durch den `TraderTraitCalculator`
|
||||||
|
schicken, NICHT persistieren.
|
||||||
|
- Engine überspringt Trade-Replay für SnapshotOnly; Estimator/Enrichment überspringen; CopytradingScore = 0
|
||||||
|
mit Trait `not_copyable_hf`.
|
||||||
|
- **Aggregated (Tier B):** Aggregation beim Import statt nachträglicher Kompaktierung: Bucket
|
||||||
|
(TraderId, MarketOutcomeId, Side, Stunde) mit VWAP-Preis, Summen-Size/-Amount, `AggregatedCount`; gespeichert
|
||||||
|
als normale Trade-Zeile mit `PlatformTradeId = "AGG_{traderId}_{outcomeId}_{side}_{yyyyMMddHH}"`, laufende
|
||||||
|
Stunde per Upsert aktualisieren. Average-Cost-Engine bleibt damit verlustfrei.
|
||||||
|
- Danach: `RetentionDays` für Full-Trader auf 180 erhöhen (Config) — die Bots stellen nicht mehr die Masse,
|
||||||
|
und längerer Track-Record nützt genau den kopierbaren Tradern.
|
||||||
|
- **Tests:** Klassifizierungs-Schwellen + Hysterese als pure Funktion; PollingWorker importiert für
|
||||||
|
SnapshotOnly-Trader nichts; Aggregations-Upsert ist idempotent (2× dieselbe Stunde → 1 Zeile, korrekte Summen
|
||||||
|
und `AggregatedCount`).
|
||||||
|
|
||||||
|
### D4. Abnahme Teil D
|
||||||
|
|
||||||
|
1. `dotnet test`: alle bestehenden **32 + 1 Skip** bleiben grün (Assertions unverändert) + die neuen D-Tests.
|
||||||
|
2. RN1/Swisstony stehen nach der Einstufung auf SnapshotOnly: PnL gefüllt (aus /positions/Leaderboard),
|
||||||
|
Traits gesetzt, **keine neuen Trade-Zeilen** mehr in der DB.
|
||||||
|
3. Detailseite zeigt Trait-Chips; Traders-Liste filterbar nach Trait.
|
||||||
|
4. Tägliches DB-Wachstum sichtbar reduziert (DB-Size-Anzeige im WinForms-Statusbar beobachten).
|
||||||
|
|
||||||
|
Reihenfolge: **D1 → D2 → D3** (bei D3 zuerst Tier C, dann Tier B).
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using Predictalytics.Domain.Enums;
|
||||||
|
using Predictalytics.Infrastructure.Helpers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Predictalytics.Application.Tests.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the keyword-based market classification (added 2026-07-11).
|
||||||
|
/// The Gamma /markets endpoint carries neither a category field nor event tags,
|
||||||
|
/// so for on-demand fetched markets the QUESTION TEXT is often the only signal —
|
||||||
|
/// the mapper must classify from it. Short tokens must match on word boundaries
|
||||||
|
/// ("eth" must not hit inside "whether", "pop" not inside "popular").
|
||||||
|
/// </summary>
|
||||||
|
public class MarketCategoryMapperTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
// Question-text-only classification (the on-demand path has no tags):
|
||||||
|
[InlineData("", "", "Will the Lakers win the NBA Finals?", MarketCategory.Sports)]
|
||||||
|
[InlineData("", "", "Ethereum above $1,850 on July 10?", MarketCategory.Crypto)]
|
||||||
|
[InlineData("", "", "Will Trump win the 2028 presidential election?", MarketCategory.Politics)]
|
||||||
|
[InlineData("", "", "Fed rate cut in September?", MarketCategory.Economy)]
|
||||||
|
[InlineData("", "", "Will SpaceX launch Starship this quarter?", MarketCategory.Science)]
|
||||||
|
[InlineData("", "", "Russia-Ukraine ceasefire before August?", MarketCategory.GlobalNews)]
|
||||||
|
[InlineData("", "", "New Rihanna album before GTA VI?", MarketCategory.PopCulture)]
|
||||||
|
// Tag-based classification still works:
|
||||||
|
[InlineData("", "Sports, NBA", "", MarketCategory.Sports)]
|
||||||
|
[InlineData("", "Crypto", "", MarketCategory.Crypto)]
|
||||||
|
// No signal at all → Other:
|
||||||
|
[InlineData("", "", "Something entirely unclassifiable happens?", MarketCategory.Other)]
|
||||||
|
public void Map_ClassifiesFromAvailableSignals(string rawCategory, string tags, string question, MarketCategory expected)
|
||||||
|
{
|
||||||
|
var (category, _) = MarketCategoryMapper.Map(rawCategory, tags, question);
|
||||||
|
Assert.Equal(expected, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Map_ShortTokens_RequireWordBoundaries()
|
||||||
|
{
|
||||||
|
// "whether" contains the substring "eth" — must NOT classify as Crypto.
|
||||||
|
var (category, _) = MarketCategoryMapper.Map("", "", "Whether the government acts by Friday?");
|
||||||
|
Assert.NotEqual(MarketCategory.Crypto, category);
|
||||||
|
|
||||||
|
// "popular vote" contains the substring "pop" — must be Politics ("vote"),
|
||||||
|
// not PopCulture.
|
||||||
|
var (category2, _) = MarketCategoryMapper.Map("", "", "Popular vote winner in Michigan?");
|
||||||
|
Assert.Equal(MarketCategory.Politics, category2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Map_Subcategory_PrefersFirstTagOverFallback()
|
||||||
|
{
|
||||||
|
var (_, subcategory) = MarketCategoryMapper.Map("", "NBA, Basketball", "Lakers to win?");
|
||||||
|
Assert.Equal("NBA", subcategory);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Predictalytics.Domain.Entities;
|
||||||
|
using Predictalytics.Domain.Enums;
|
||||||
|
using Predictalytics.Infrastructure.Data;
|
||||||
|
using Predictalytics.Infrastructure.Data.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Predictalytics.Application.Tests.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the market upsert paths (added 2026-07-11).
|
||||||
|
/// Uses SQLite in-memory (same pattern as TradeRetentionWorkerTests).
|
||||||
|
/// </summary>
|
||||||
|
public class MarketRepositoryTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The on-demand /markets fetch carries no event tags and classifies markets
|
||||||
|
/// as "Other". Such an uninformative update must NOT overwrite a category that
|
||||||
|
/// was previously derived from the tag-bearing /events sync — while a
|
||||||
|
/// legitimate reclassification (Other → Sports) must still go through.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task AddOrUpdateAsync_DoesNotOverwriteGoodCategoryWithOther()
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection("DataSource=:memory:");
|
||||||
|
connection.Open();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using (var setup = new AppDbContext(options))
|
||||||
|
{
|
||||||
|
setup.Database.EnsureCreated();
|
||||||
|
var ev = new Event { Id = 1, Platform = PlatformType.Polymarket, Slug = "e", Title = "E" };
|
||||||
|
setup.Set<Event>().Add(ev);
|
||||||
|
setup.Markets.Add(new Market
|
||||||
|
{
|
||||||
|
Id = 10, EventId = 1, Platform = PlatformType.Polymarket,
|
||||||
|
ConditionId = "0xabc", PlatformMarketId = 1L, Question = "Lakers to win?",
|
||||||
|
Category = MarketCategory.Sports, Subcategory = "NBA"
|
||||||
|
});
|
||||||
|
setup.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 1: uninformative on-demand update (no tags → Other) must not downgrade.
|
||||||
|
using (var ctx = new AppDbContext(options))
|
||||||
|
{
|
||||||
|
var repo = new MarketRepository(ctx);
|
||||||
|
await repo.AddOrUpdateAsync(new Market
|
||||||
|
{
|
||||||
|
Platform = PlatformType.Polymarket, ConditionId = "0xabc",
|
||||||
|
PlatformMarketId = 1L, Question = "Lakers to win?",
|
||||||
|
Category = MarketCategory.Other, Subcategory = "Other"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var assertCtx = new AppDbContext(options))
|
||||||
|
{
|
||||||
|
var market = await assertCtx.Markets.SingleAsync(m => m.ConditionId == "0xabc");
|
||||||
|
Assert.Equal(MarketCategory.Sports, market.Category);
|
||||||
|
Assert.Equal("NBA", market.Subcategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 2: a real classification must still overwrite an existing "Other".
|
||||||
|
using (var ctx = new AppDbContext(options))
|
||||||
|
{
|
||||||
|
var repo = new MarketRepository(ctx);
|
||||||
|
await repo.AddOrUpdateAsync(new Market
|
||||||
|
{
|
||||||
|
Platform = PlatformType.Polymarket, ConditionId = "0xdef",
|
||||||
|
PlatformMarketId = 2L, Question = "Unknown thing?",
|
||||||
|
Category = MarketCategory.Other, Subcategory = "Other",
|
||||||
|
Event = new Event { Platform = PlatformType.Polymarket, PlatformEventId = 2, Slug = "e2", Title = "E2" }
|
||||||
|
});
|
||||||
|
await repo.AddOrUpdateAsync(new Market
|
||||||
|
{
|
||||||
|
Platform = PlatformType.Polymarket, ConditionId = "0xdef",
|
||||||
|
PlatformMarketId = 2L, Question = "Unknown thing?",
|
||||||
|
Category = MarketCategory.Politics, Subcategory = "Elections"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var assertCtx = new AppDbContext(options))
|
||||||
|
{
|
||||||
|
var market = await assertCtx.Markets.SingleAsync(m => m.ConditionId == "0xdef");
|
||||||
|
Assert.Equal(MarketCategory.Politics, market.Category);
|
||||||
|
Assert.Equal("Elections", market.Subcategory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -778,4 +778,59 @@ public class PositionPnLEngineTests
|
|||||||
Assert.Equal(30m, analytics.OverallPnL);
|
Assert.Equal(30m, analytics.OverallPnL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Added 2026-07-11: Trader.TotalTrades was a drifting increment counter
|
||||||
|
/// (INSERT IGNORE over-counts, historic imports never counted, deletions never
|
||||||
|
/// subtracted), which produced impossible states like Trades30d > TotalTrades.
|
||||||
|
/// The engine recalculation is the single source of truth: it must sync
|
||||||
|
/// TotalTrades to the actual number of trade rows it just replayed.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task RecalculateTraderPositionsAsync_SyncsTotalTradesWithActualTradeCount()
|
||||||
|
{
|
||||||
|
// Arrange: counter is wildly wrong (both directions occur in production).
|
||||||
|
var dbName = Guid.NewGuid().ToString();
|
||||||
|
using (var db = CreateDbContext(dbName))
|
||||||
|
{
|
||||||
|
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1", TotalTrades = 999 };
|
||||||
|
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
|
||||||
|
market.Outcomes.Add(new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.50m });
|
||||||
|
db.Traders.Add(trader);
|
||||||
|
db.Markets.Add(market);
|
||||||
|
|
||||||
|
db.Trades.Add(new Trade
|
||||||
|
{
|
||||||
|
Id = 10, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||||
|
Side = TradeSide.Buy, Price = 0.40m, Size = 100m, Amount = 40m,
|
||||||
|
ExecutedAt = DateTime.UtcNow.AddDays(-2)
|
||||||
|
});
|
||||||
|
db.Trades.Add(new Trade
|
||||||
|
{
|
||||||
|
Id = 11, TraderId = 1, DbMarketId = 10, MarketOutcomeId = 100,
|
||||||
|
Side = TradeSide.Sell, Price = 0.50m, Size = 50m, Amount = 25m,
|
||||||
|
ExecutedAt = DateTime.UtcNow.AddDays(-1)
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
using (var db = CreateDbContext(dbName))
|
||||||
|
{
|
||||||
|
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
|
||||||
|
await pnlEngine.RecalculateTraderPositionsAsync(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert: counter equals the real row count, and the windowed count can
|
||||||
|
// never exceed it again.
|
||||||
|
using (var db = CreateDbContext(dbName))
|
||||||
|
{
|
||||||
|
var trader = await db.Traders.SingleAsync(t => t.Id == 1);
|
||||||
|
Assert.Equal(2, trader.TotalTrades);
|
||||||
|
|
||||||
|
var analytics = await db.TraderAnalytics.SingleAsync(a => a.TraderId == 1);
|
||||||
|
Assert.True(analytics.Trades30d <= trader.TotalTrades,
|
||||||
|
$"Trades30d ({analytics.Trades30d}) must never exceed TotalTrades ({trader.TotalTrades})");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,8 +192,15 @@ public class MarketRepository : IMarketRepository
|
|||||||
existing.PlatformMarketId = updated.PlatformMarketId;
|
existing.PlatformMarketId = updated.PlatformMarketId;
|
||||||
existing.QuestionId = updated.QuestionId;
|
existing.QuestionId = updated.QuestionId;
|
||||||
existing.Description = updated.Description;
|
existing.Description = updated.Description;
|
||||||
|
|
||||||
|
// The /markets endpoint (on-demand path) carries no event tags, so its
|
||||||
|
// classification is often just "Other". Never let an uninformative update
|
||||||
|
// overwrite a category previously derived from the tag-bearing /events sync.
|
||||||
|
if (updated.Category != MarketCategory.Other || existing.Category == MarketCategory.Other)
|
||||||
|
{
|
||||||
existing.Category = updated.Category;
|
existing.Category = updated.Category;
|
||||||
existing.Subcategory = updated.Subcategory;
|
existing.Subcategory = updated.Subcategory;
|
||||||
|
}
|
||||||
existing.Volume = updated.Volume;
|
existing.Volume = updated.Volume;
|
||||||
existing.Volume24h = updated.Volume24h;
|
existing.Volume24h = updated.Volume24h;
|
||||||
existing.Liquidity = updated.Liquidity;
|
existing.Liquidity = updated.Liquidity;
|
||||||
|
|||||||
@@ -1,33 +1,77 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
using Predictalytics.Domain.Enums;
|
using Predictalytics.Domain.Enums;
|
||||||
|
|
||||||
namespace Predictalytics.Infrastructure.Helpers;
|
namespace Predictalytics.Infrastructure.Helpers;
|
||||||
|
|
||||||
public static class MarketCategoryMapper
|
public static class MarketCategoryMapper
|
||||||
{
|
{
|
||||||
public static (MarketCategory Category, string Subcategory) Map(string rawCategory, string tags)
|
/// <summary>
|
||||||
|
/// Classification rules, first match wins. Two matching modes per category:
|
||||||
|
/// - Substrings: long, unambiguous fragments matched anywhere ("politic", "bitcoin").
|
||||||
|
/// - Words: short/ambiguous tokens matched only on word boundaries, so "eth"
|
||||||
|
/// cannot hit inside "whether" and "pop" cannot hit inside "popular".
|
||||||
|
/// </summary>
|
||||||
|
private static readonly (MarketCategory Category, string[] Substrings, string[] Words)[] Rules =
|
||||||
{
|
{
|
||||||
var searchString = $"{rawCategory} {tags}".ToLowerInvariant();
|
(MarketCategory.Politics,
|
||||||
|
new[] { "politic", "election", "president", "senat", "congress", "parliament", "impeach",
|
||||||
|
"referendum", "governor", "minister", "chancellor", "nominee", "supreme court",
|
||||||
|
"white house", "ballot", "veto", "coalition", "legislation", "mayor" },
|
||||||
|
new[] { "trump", "biden", "harris", "vance", "scotus", "gop", "dnc", "rnc", "poll", "polls", "vote", "votes" }),
|
||||||
|
|
||||||
if (searchString.Contains("politic") || searchString.Contains("election") || searchString.Contains("trump") || searchString.Contains("biden"))
|
(MarketCategory.Crypto,
|
||||||
return (MarketCategory.Politics, GetSubcategory(rawCategory, tags, "Elections"));
|
new[] { "crypto", "bitcoin", "ethereum", "solana", "dogecoin", "blockchain", "stablecoin",
|
||||||
|
"binance", "coinbase", "airdrop", "halving", "memecoin", "altcoin", "defi", "satoshi" },
|
||||||
|
new[] { "btc", "eth", "sol", "xrp", "doge", "nft", "bnb", "ada", "usdt", "usdc" }),
|
||||||
|
|
||||||
if (searchString.Contains("crypto") || searchString.Contains("bitcoin") || searchString.Contains("eth") || searchString.Contains("solana"))
|
(MarketCategory.Sports,
|
||||||
return (MarketCategory.Crypto, GetSubcategory(rawCategory, tags, "Crypto"));
|
new[] { "sport", "soccer", "tennis", "basketball", "baseball", "football", "hockey", "olympic",
|
||||||
|
"champions league", "premier league", "bundesliga", "la liga", "serie a", "world cup",
|
||||||
|
"super bowl", "grand slam", "wimbledon", "playoff", "esport", "cricket", "rugby",
|
||||||
|
"golf", "boxing", "marathon", "formula 1", "grand prix", "stanley cup", "world series",
|
||||||
|
"roland garros", "us open" },
|
||||||
|
new[] { "nfl", "nba", "mlb", "nhl", "ufc", "mma", "f1", "fifa", "uefa", "atp", "wta", "pga", "ncaa", "epl" }),
|
||||||
|
|
||||||
if (searchString.Contains("sport") || searchString.Contains("nfl") || searchString.Contains("nba") || searchString.Contains("soccer") || searchString.Contains("tennis"))
|
(MarketCategory.PopCulture,
|
||||||
return (MarketCategory.Sports, GetSubcategory(rawCategory, tags, "Sports"));
|
new[] { "culture", "movie", "oscars", "music", "album", "billboard", "box office", "netflix",
|
||||||
|
"spotify", "grammy", "emmy", "celebrit", "taylor swift", "mrbeast", "youtube", "tiktok",
|
||||||
|
"video game", "rotten tomatoes", "eurovision" },
|
||||||
|
new[] { "pop", "gta", "oscar" }),
|
||||||
|
|
||||||
if (searchString.Contains("pop") || searchString.Contains("culture") || searchString.Contains("movie") || searchString.Contains("oscars") || searchString.Contains("music"))
|
(MarketCategory.Science,
|
||||||
return (MarketCategory.PopCulture, GetSubcategory(rawCategory, tags, "Pop Culture"));
|
new[] { "science", "spacex", "nasa", "asteroid", "hurricane", "earthquake", "climate", "weather",
|
||||||
|
"temperature", "vaccine", "pandemic", "artificial intelligence", "openai", "chatgpt",
|
||||||
|
"quantum", "starship", "nobel", "space" },
|
||||||
|
new[] { "ai", "agi", "gpt", "llm" }),
|
||||||
|
|
||||||
if (searchString.Contains("science") || searchString.Contains("space") || searchString.Contains("weather") || searchString.Contains("climate"))
|
(MarketCategory.GlobalNews,
|
||||||
return (MarketCategory.Science, GetSubcategory(rawCategory, tags, "Science"));
|
new[] { "global", "world", "news", "ceasefire", "ukraine", "russia", "israel", "gaza", "iran",
|
||||||
|
"taiwan", "nato", "sanction", "treaty", "invasion", "north korea", "hostage",
|
||||||
|
"military", "missile", "nuclear" },
|
||||||
|
new[] { "war", "u.n." }),
|
||||||
|
|
||||||
if (searchString.Contains("news") || searchString.Contains("global") || searchString.Contains("world"))
|
(MarketCategory.Economy,
|
||||||
return (MarketCategory.GlobalNews, GetSubcategory(rawCategory, tags, "Global News"));
|
new[] { "econom", "finance", "business", "inflation", "recession", "interest rate", "rate cut",
|
||||||
|
"rate hike", "unemployment", "tariff", "treasury", "earnings", "market cap",
|
||||||
|
"stock price", "bankrupt", "acquisition", "merger" },
|
||||||
|
new[] { "fed", "fomc", "cpi", "gdp", "nasdaq", "dow", "ipo" }),
|
||||||
|
};
|
||||||
|
|
||||||
if (searchString.Contains("economy") || searchString.Contains("finance") || searchString.Contains("business") || searchString.Contains("fed"))
|
/// <summary>
|
||||||
return (MarketCategory.Economy, GetSubcategory(rawCategory, tags, "Economy"));
|
/// Classifies a market. The question text is a first-class signal: the Gamma
|
||||||
|
/// /markets endpoint delivers neither a category field nor event tags, so for
|
||||||
|
/// on-demand fetched markets the question is often the ONLY signal available.
|
||||||
|
/// </summary>
|
||||||
|
public static (MarketCategory Category, string Subcategory) Map(string rawCategory, string tags, string question = "")
|
||||||
|
{
|
||||||
|
var searchString = $"{rawCategory} {tags} {question}".ToLowerInvariant();
|
||||||
|
var tokens = new HashSet<string>(Regex.Split(searchString, "[^a-z0-9.]+"));
|
||||||
|
|
||||||
|
foreach (var rule in Rules)
|
||||||
|
{
|
||||||
|
if (rule.Words.Any(tokens.Contains) || rule.Substrings.Any(searchString.Contains))
|
||||||
|
return (rule.Category, GetSubcategory(rawCategory, tags, rule.Category.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
return (MarketCategory.Other, GetSubcategory(rawCategory, tags, "Other"));
|
return (MarketCategory.Other, GetSubcategory(rawCategory, tags, "Other"));
|
||||||
}
|
}
|
||||||
@@ -35,7 +79,7 @@ public static class MarketCategoryMapper
|
|||||||
private static string GetSubcategory(string rawCategory, string tags, string fallback)
|
private static string GetSubcategory(string rawCategory, string tags, string fallback)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(rawCategory) && !rawCategory.Equals("OVERALL", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrWhiteSpace(rawCategory) && !rawCategory.Equals("OVERALL", StringComparison.OrdinalIgnoreCase))
|
||||||
return rawCategory;
|
return rawCategory.Trim();
|
||||||
|
|
||||||
var firstTag = tags.Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim();
|
var firstTag = tags.Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(firstTag))
|
if (!string.IsNullOrWhiteSpace(firstTag))
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ public class PolymarketProvider : IPlatformProvider
|
|||||||
{
|
{
|
||||||
long.TryParse(raw.Id, out var marketNumericId);
|
long.TryParse(raw.Id, out var marketNumericId);
|
||||||
|
|
||||||
var catMap = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(raw.Category ?? "", parentTags);
|
var catMap = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(raw.Category ?? "", parentTags, raw.Question ?? "");
|
||||||
|
|
||||||
var market = new Market
|
var market = new Market
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -367,6 +367,11 @@ public class PositionPnLEngine : IPositionPnLEngine
|
|||||||
// Sync back to Trader record for quick sorting / UI display
|
// Sync back to Trader record for quick sorting / UI display
|
||||||
trader.TotalPnl = overallPnl;
|
trader.TotalPnl = overallPnl;
|
||||||
trader.WinRate = winRateOverall;
|
trader.WinRate = winRateOverall;
|
||||||
|
// Row count is the single source of truth for TotalTrades — the worker-side
|
||||||
|
// increment counters drift (INSERT IGNORE, deletions, historic imports) and
|
||||||
|
// produced impossible states like Trades30d > TotalTrades. Compacted rows
|
||||||
|
// count as 1 until an AggregatedCount column exists (FIXPLAN Teil D).
|
||||||
|
trader.TotalTrades = trades.Count;
|
||||||
if (trades.Count > 0 || trader.LastTradesUpdatedAt != null)
|
if (trades.Count > 0 || trader.LastTradesUpdatedAt != null)
|
||||||
{
|
{
|
||||||
trader.LastAnalyzedAt = DateTime.UtcNow;
|
trader.LastAnalyzedAt = DateTime.UtcNow;
|
||||||
|
|||||||
+13
-166
@@ -23,162 +23,7 @@ partial class MainForm
|
|||||||
statusStrip1 = new StatusStrip();
|
statusStrip1 = new StatusStrip();
|
||||||
label_apiRatelimit = new ToolStripStatusLabel();
|
label_apiRatelimit = new ToolStripStatusLabel();
|
||||||
label_buildVersion = new ToolStripStatusLabel();
|
label_buildVersion = new ToolStripStatusLabel();
|
||||||
tabControl1 = new TabControl();
|
|
||||||
tabPage_terminal = new TabPage();
|
|
||||||
rtb_terminal = new RichTextBox();
|
|
||||||
tabPage2 = new TabPage();
|
|
||||||
pg_settings = new PropertyGrid();
|
|
||||||
menuStrip1 = new MenuStrip();
|
|
||||||
filesToolStripMenuItem = new ToolStripMenuItem();
|
|
||||||
editToolStripMenuItem = new ToolStripMenuItem();
|
|
||||||
btn_logfolder = new ToolStripMenuItem();
|
|
||||||
btn_openbrowser = new ToolStripMenuItem();
|
|
||||||
developmentToolStripMenuItem = new ToolStripMenuItem();
|
|
||||||
btn_dbReset = new ToolStripMenuItem();
|
|
||||||
btn_syncmarkets = new ToolStripMenuItem();
|
|
||||||
btn_dbUpdate = new ToolStripMenuItem();
|
|
||||||
btn_recalcAll = new ToolStripMenuItem();
|
|
||||||
toolStrip1.SuspendLayout();
|
|
||||||
statusStrip1.SuspendLayout();
|
|
||||||
tabControl1.SuspendLayout();
|
|
||||||
tabPage_terminal.SuspendLayout();
|
|
||||||
tabPage2.SuspendLayout();
|
|
||||||
menuStrip1.SuspendLayout();
|
|
||||||
SuspendLayout();
|
|
||||||
//
|
|
||||||
// toolStrip1
|
|
||||||
//
|
|
||||||
toolStrip1.ImageScalingSize = new Size(24, 24);
|
|
||||||
toolStrip1.Items.AddRange(new ToolStripItem[] { btn_serverstart, btn_localWebserver });
|
|
||||||
toolStrip1.Location = new Point(0, 33);
|
|
||||||
toolStrip1.Name = "toolStrip1";
|
|
||||||
toolStrip1.Size = new Size(1864, 34);
|
|
||||||
toolStrip1.TabIndex = 0;
|
|
||||||
//
|
|
||||||
// btn_serverstart
|
|
||||||
//
|
|
||||||
btn_serverstart.ImageTransparentColor = Color.Magenta;
|
|
||||||
btn_serverstart.Name = "btn_serverstart";
|
|
||||||
btn_serverstart.Size = new Size(127, 29);
|
|
||||||
btn_serverstart.Text = "▶ Start Server";
|
|
||||||
//
|
|
||||||
// btn_localWebserver
|
|
||||||
//
|
|
||||||
btn_localWebserver.ImageTransparentColor = Color.Magenta;
|
|
||||||
btn_localWebserver.Name = "btn_localWebserver";
|
|
||||||
btn_localWebserver.Size = new Size(161, 29);
|
|
||||||
btn_localWebserver.Text = "▶ Start Webserver";
|
|
||||||
//
|
|
||||||
// statusStrip1
|
|
||||||
//
|
|
||||||
statusStrip1.ImageScalingSize = new Size(24, 24);
|
|
||||||
statusStrip1.Items.AddRange(new ToolStripItem[] { label_apiRatelimit, label_buildVersion });
|
|
||||||
statusStrip1.Location = new Point(0, 1000);
|
|
||||||
statusStrip1.Name = "statusStrip1";
|
|
||||||
statusStrip1.Size = new Size(1864, 32);
|
|
||||||
statusStrip1.TabIndex = 1;
|
|
||||||
//
|
|
||||||
// label_apiRatelimit
|
|
||||||
//
|
|
||||||
label_apiRatelimit.Name = "label_apiRatelimit";
|
|
||||||
label_apiRatelimit.Size = new Size(1782, 25);
|
|
||||||
label_apiRatelimit.Spring = true;
|
|
||||||
label_apiRatelimit.Text = "API: OK";
|
|
||||||
label_apiRatelimit.TextAlign = ContentAlignment.MiddleLeft;
|
|
||||||
//
|
|
||||||
// label_buildVersion
|
|
||||||
//
|
|
||||||
label_buildVersion.Name = "label_buildVersion";
|
|
||||||
label_buildVersion.Size = new Size(67, 25);
|
|
||||||
label_buildVersion.Text = "Build: -";
|
|
||||||
label_buildVersion.TextAlign = ContentAlignment.MiddleRight;
|
|
||||||
//
|
|
||||||
// tabControl1
|
|
||||||
//
|
|
||||||
tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
|
||||||
tabControl1.Controls.Add(tabPage_terminal);
|
|
||||||
tabControl1.Controls.Add(tabPage2);
|
|
||||||
tabControl1.Location = new Point(0, 61);
|
|
||||||
tabControl1.Name = "tabControl1";
|
|
||||||
tabControl1.SelectedIndex = 0;
|
|
||||||
tabControl1.Size = new Size(1864, 946);
|
|
||||||
tabControl1.TabIndex = 2;
|
|
||||||
//
|
|
||||||
// tabPage_terminal
|
|
||||||
//
|
|
||||||
tabPage_terminal.Controls.Add(rtb_terminal);
|
|
||||||
tabPage_terminal.Location = new Point(4, 34);
|
|
||||||
tabPage_terminal.Name = "tabPage_terminal";
|
|
||||||
tabPage_terminal.Padding = new Padding(3);
|
|
||||||
tabPage_terminal.Size = new Size(1856, 908);
|
|
||||||
tabPage_terminal.TabIndex = 0;
|
|
||||||
tabPage_terminal.Text = "Terminal";
|
|
||||||
tabPage_terminal.UseVisualStyleBackColor = true;
|
|
||||||
//
|
|
||||||
// rtb_terminal
|
|
||||||
//
|
|
||||||
rtb_terminal.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
|
||||||
rtb_terminal.Location = new Point(3, 6);
|
|
||||||
rtb_terminal.Name = "rtb_terminal";
|
|
||||||
rtb_terminal.Size = new Size(1847, 896);
|
|
||||||
rtb_terminal.TabIndex = 0;
|
|
||||||
rtb_terminal.Text = "";
|
|
||||||
//
|
|
||||||
// tabPage2
|
|
||||||
//
|
|
||||||
tabPage2.Controls.Add(pg_settings);
|
|
||||||
tabPage2.Location = new Point(4, 34);
|
|
||||||
tabPage2.Name = "tabPage2";
|
|
||||||
tabPage2.Padding = new Padding(3);
|
|
||||||
tabPage2.Size = new Size(1856, 908);
|
|
||||||
tabPage2.TabIndex = 1;
|
|
||||||
tabPage2.Text = "Settings";
|
|
||||||
tabPage2.UseVisualStyleBackColor = true;
|
|
||||||
//
|
|
||||||
// pg_settings
|
|
||||||
//
|
|
||||||
pg_settings.Location = new Point(3, 6);
|
|
||||||
pg_settings.Name = "pg_settings";
|
|
||||||
pg_settings.Size = new Size(1850, 896);
|
|
||||||
pg_settings.TabIndex = 0;
|
|
||||||
//
|
|
||||||
// menuStrip1
|
|
||||||
//
|
|
||||||
menuStrip1.ImageScalingSize = new Size(24, 24);
|
|
||||||
menuStrip1.Items.AddRange(new ToolStripItem[] { filesToolStripMenuItem, editToolStripMenuItem, developmentToolStripMenuItem });
|
|
||||||
menuStrip1.Location = new Point(0, 0);
|
|
||||||
menuStrip1.Name = "menuStrip1";
|
|
||||||
menuStrip1.Size = new Size(1864, 33);
|
|
||||||
menuStrip1.TabIndex = 3;
|
|
||||||
//
|
|
||||||
// filesToolStripMenuItem
|
|
||||||
//
|
|
||||||
filesToolStripMenuItem.Name = "filesToolStripMenuItem";
|
|
||||||
filesToolStripMenuItem.Size = new Size(62, 29);
|
|
||||||
filesToolStripMenuItem.Text = "Files";
|
|
||||||
//
|
|
||||||
// editToolStripMenuItem
|
|
||||||
//
|
|
||||||
editToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { btn_logfolder, btn_openbrowser });
|
|
||||||
editToolStripMenuItem.Name = "editToolStripMenuItem";
|
|
||||||
editToolStripMenuItem.Size = new Size(58, 29);
|
|
||||||
editToolStripMenuItem.Text = "Edit";
|
|
||||||
//
|
|
||||||
// btn_logfolder
|
|
||||||
//
|
|
||||||
btn_logfolder.Name = "btn_logfolder";
|
|
||||||
btn_logfolder.Size = new Size(261, 34);
|
|
||||||
btn_logfolder.Text = "Show Logfolder";
|
|
||||||
btn_logfolder.Click += btn_logfolder_Click;
|
|
||||||
//
|
|
||||||
// btn_openbrowser
|
|
||||||
toolStrip1 = new ToolStrip();
|
|
||||||
btn_serverstart = new ToolStripButton();
|
|
||||||
btn_localWebserver = new ToolStripButton();
|
|
||||||
statusStrip1 = new StatusStrip();
|
|
||||||
label_apiRatelimit = new ToolStripStatusLabel();
|
|
||||||
label_dbSize = new ToolStripStatusLabel();
|
label_dbSize = new ToolStripStatusLabel();
|
||||||
label_buildVersion = new ToolStripStatusLabel();
|
|
||||||
tabControl1 = new TabControl();
|
tabControl1 = new TabControl();
|
||||||
tabPage_terminal = new TabPage();
|
tabPage_terminal = new TabPage();
|
||||||
rtb_terminal = new RichTextBox();
|
rtb_terminal = new RichTextBox();
|
||||||
@@ -242,13 +87,6 @@ partial class MainForm
|
|||||||
label_apiRatelimit.Text = "API: OK";
|
label_apiRatelimit.Text = "API: OK";
|
||||||
label_apiRatelimit.TextAlign = ContentAlignment.MiddleLeft;
|
label_apiRatelimit.TextAlign = ContentAlignment.MiddleLeft;
|
||||||
//
|
//
|
||||||
// label_dbSize
|
|
||||||
//
|
|
||||||
label_dbSize.Name = "label_dbSize";
|
|
||||||
label_dbSize.Size = new Size(150, 25);
|
|
||||||
label_dbSize.Text = "DB Size: -";
|
|
||||||
label_dbSize.TextAlign = ContentAlignment.MiddleRight;
|
|
||||||
//
|
|
||||||
// label_buildVersion
|
// label_buildVersion
|
||||||
//
|
//
|
||||||
label_buildVersion.Name = "label_buildVersion";
|
label_buildVersion.Name = "label_buildVersion";
|
||||||
@@ -256,6 +94,13 @@ partial class MainForm
|
|||||||
label_buildVersion.Text = "Build: -";
|
label_buildVersion.Text = "Build: -";
|
||||||
label_buildVersion.TextAlign = ContentAlignment.MiddleRight;
|
label_buildVersion.TextAlign = ContentAlignment.MiddleRight;
|
||||||
//
|
//
|
||||||
|
// label_dbSize
|
||||||
|
//
|
||||||
|
label_dbSize.Name = "label_dbSize";
|
||||||
|
label_dbSize.Size = new Size(150, 25);
|
||||||
|
label_dbSize.Text = "DB Size: -";
|
||||||
|
label_dbSize.TextAlign = ContentAlignment.MiddleRight;
|
||||||
|
//
|
||||||
// tabControl1
|
// tabControl1
|
||||||
//
|
//
|
||||||
tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||||
@@ -351,27 +196,27 @@ partial class MainForm
|
|||||||
// btn_dbReset
|
// btn_dbReset
|
||||||
//
|
//
|
||||||
btn_dbReset.Name = "btn_dbReset";
|
btn_dbReset.Name = "btn_dbReset";
|
||||||
btn_dbReset.Size = new Size(270, 34);
|
btn_dbReset.Size = new Size(286, 34);
|
||||||
btn_dbReset.Text = "reset TradesDB";
|
btn_dbReset.Text = "reset TradesDB";
|
||||||
//
|
//
|
||||||
// btn_syncmarkets
|
// btn_syncmarkets
|
||||||
//
|
//
|
||||||
btn_syncmarkets.Name = "btn_syncmarkets";
|
btn_syncmarkets.Name = "btn_syncmarkets";
|
||||||
btn_syncmarkets.Size = new Size(270, 34);
|
btn_syncmarkets.Size = new Size(286, 34);
|
||||||
btn_syncmarkets.Text = "Sync Markets";
|
btn_syncmarkets.Text = "Sync Markets";
|
||||||
btn_syncmarkets.Click += syncMarketsaToolStripMenuItem_Click;
|
btn_syncmarkets.Click += syncMarketsaToolStripMenuItem_Click;
|
||||||
//
|
//
|
||||||
// btn_dbUpdate
|
// btn_dbUpdate
|
||||||
//
|
//
|
||||||
btn_dbUpdate.Name = "btn_dbUpdate";
|
btn_dbUpdate.Name = "btn_dbUpdate";
|
||||||
btn_dbUpdate.Size = new Size(270, 34);
|
btn_dbUpdate.Size = new Size(286, 34);
|
||||||
btn_dbUpdate.Text = "UpdateDB";
|
btn_dbUpdate.Text = "UpdateDB";
|
||||||
btn_dbUpdate.Click += btn_dbUpdate_Click;
|
btn_dbUpdate.Click += btn_dbUpdate_Click;
|
||||||
//
|
//
|
||||||
// btn_recalcAll
|
// btn_recalcAll
|
||||||
//
|
//
|
||||||
btn_recalcAll.Name = "btn_recalcAll";
|
btn_recalcAll.Name = "btn_recalcAll";
|
||||||
btn_recalcAll.Size = new Size(270, 34);
|
btn_recalcAll.Size = new Size(286, 34);
|
||||||
btn_recalcAll.Text = "Recalculate All Traders";
|
btn_recalcAll.Text = "Recalculate All Traders";
|
||||||
btn_recalcAll.Click += btn_recalcAll_Click;
|
btn_recalcAll.Click += btn_recalcAll_Click;
|
||||||
//
|
//
|
||||||
@@ -385,6 +230,8 @@ partial class MainForm
|
|||||||
Controls.Add(toolStrip1);
|
Controls.Add(toolStrip1);
|
||||||
Controls.Add(menuStrip1);
|
Controls.Add(menuStrip1);
|
||||||
MainMenuStrip = menuStrip1;
|
MainMenuStrip = menuStrip1;
|
||||||
|
MaximumSize = new Size(1886, 1088);
|
||||||
|
MinimumSize = new Size(1886, 1088);
|
||||||
Name = "MainForm";
|
Name = "MainForm";
|
||||||
Text = "Predictalytics";
|
Text = "Predictalytics";
|
||||||
toolStrip1.ResumeLayout(false);
|
toolStrip1.ResumeLayout(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user