Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
59d34d0
work in progress
CostaFot Jun 20, 2026
5ddd782
wip
CostaFot Jun 20, 2026
96b064e
wip
CostaFot Jun 20, 2026
0ebeb23
wip
CostaFot Jun 20, 2026
c21af8e
wip
CostaFot Jun 20, 2026
ed27215
wip
CostaFot Jun 20, 2026
a83a850
wip
CostaFot Jun 20, 2026
b434c15
wip
CostaFot Jun 20, 2026
38f9919
wip
CostaFot Jun 20, 2026
8fea6b6
wip
CostaFot Jun 20, 2026
bad5dc2
wip
CostaFot Jun 21, 2026
987e835
wip
CostaFot Jun 21, 2026
657950d
wip
CostaFot Jun 21, 2026
4d62129
wip
CostaFot Jun 22, 2026
07b0259
wip
CostaFot Jun 22, 2026
c48569c
wip
CostaFot Jun 22, 2026
e74e978
wip
CostaFot Jun 22, 2026
ef108bb
wip
CostaFot Jun 22, 2026
693150e
wip
CostaFot Jun 22, 2026
ef8faea
wip
CostaFot Jun 22, 2026
a2f9caf
wip
CostaFot Jun 22, 2026
622ecfd
wip
CostaFot Jun 22, 2026
2d13b43
wip
CostaFot Jun 22, 2026
3bd33ba
wip
CostaFot Jun 22, 2026
3e75407
wip
CostaFot Jun 24, 2026
2ead3c7
wip
CostaFot Jun 24, 2026
2d1ae77
add chat auto-update
CostaFot Jun 24, 2026
8bebb44
convert to hub
CostaFot Jun 24, 2026
9e07f1b
add settings page
CostaFot Jun 24, 2026
700c8a4
add 12data
CostaFot Jun 24, 2026
5297164
add 12data
CostaFot Jun 24, 2026
ebeb497
add legal stuff
CostaFot Jun 24, 2026
d17ccb4
add hint for keys
CostaFot Jun 24, 2026
d6d1edf
add hint for keys
CostaFot Jun 24, 2026
0bfb4b4
gridlines on chart
CostaFot Jun 24, 2026
f4e2297
gridlines on chart
CostaFot Jun 24, 2026
d0d3c62
updates
CostaFot Jun 24, 2026
85c581a
convert to RX
CostaFot Jun 24, 2026
1f0d8ba
convert to RX
CostaFot Jun 25, 2026
a6401f5
add rate limit handling
CostaFot Jun 25, 2026
9181cc3
placeholder portfolio setup
CostaFot Jun 25, 2026
8b00a41
wip portfolio
CostaFot Jun 25, 2026
8aef44b
wip portfolio
CostaFot Jun 25, 2026
01a18dd
portfolio add currency handling and totals
CostaFot Jun 25, 2026
977bc0f
portfolio dockband
CostaFot Jun 25, 2026
4af29bd
portfolio dockband
CostaFot Jun 25, 2026
aa37bf9
updates
CostaFot Jun 25, 2026
8e01e39
add demo mode
CostaFot Jun 25, 2026
09a291b
add demo mode
CostaFot Jun 25, 2026
1071a44
add demo mode
CostaFot Jun 25, 2026
c4a2b0b
add demo mode
CostaFot Jun 25, 2026
3de96eb
add demo mode
CostaFot Jun 25, 2026
6ae09b1
add demo mode
CostaFot Jun 25, 2026
2aed25c
assets
CostaFot Jun 25, 2026
4e4d835
assets
CostaFot Jun 25, 2026
a3659ae
strings
CostaFot Jun 25, 2026
c3ba377
localization
CostaFot Jun 25, 2026
a0c5b65
localization
CostaFot Jun 25, 2026
510c67c
localization
CostaFot Jun 25, 2026
2e3d37d
localization
CostaFot Jun 25, 2026
ae1c2e5
localization
CostaFot Jun 25, 2026
d7b0e88
localization
CostaFot Jun 25, 2026
b463a36
localization
CostaFot Jun 25, 2026
8b4478b
localization
CostaFot Jun 25, 2026
215c92b
alpha prep
CostaFot Jun 25, 2026
45a8a8d
alpha prep
CostaFot Jun 25, 2026
2819f31
alpha prep
CostaFot Jun 25, 2026
ef4137d
alpha prep
CostaFot Jun 25, 2026
cbd3410
alpha prep
CostaFot Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,021 changes: 1,011 additions & 10 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.20250829.1" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
<PackageVersion Include="Sentry" Version="6.4.1" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.2.1" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
</ItemGroup>
Expand Down
Binary file modified MarketExtension/Assets/LargeTile.scale-100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/LargeTile.scale-125.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/LargeTile.scale-150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/LargeTile.scale-200.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/LargeTile.scale-400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SmallTile.scale-100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SmallTile.scale-125.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SmallTile.scale-150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SmallTile.scale-200.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SmallTile.scale-400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SplashScreen.scale-100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SplashScreen.scale-125.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SplashScreen.scale-150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SplashScreen.scale-200.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/SplashScreen.scale-400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/Square150x150Logo.scale-100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/Square150x150Logo.scale-125.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/Square150x150Logo.scale-150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/Square150x150Logo.scale-200.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/Square150x150Logo.scale-400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified MarketExtension/Assets/Square44x44Logo.scale-100.png
Binary file modified MarketExtension/Assets/Square44x44Logo.scale-125.png
Binary file modified MarketExtension/Assets/Square44x44Logo.scale-150.png
Binary file modified MarketExtension/Assets/Square44x44Logo.scale-200.png
Binary file modified MarketExtension/Assets/Square44x44Logo.scale-400.png
Binary file modified MarketExtension/Assets/Square44x44Logo.targetsize-16.png
Binary file modified MarketExtension/Assets/Square44x44Logo.targetsize-24.png
Binary file modified MarketExtension/Assets/Square44x44Logo.targetsize-256.png
Binary file modified MarketExtension/Assets/Square44x44Logo.targetsize-32.png
Binary file modified MarketExtension/Assets/Square44x44Logo.targetsize-48.png
Binary file modified MarketExtension/Assets/StoreLogo.scale-100.png
Binary file modified MarketExtension/Assets/StoreLogo.scale-125.png
Binary file modified MarketExtension/Assets/StoreLogo.scale-150.png
Binary file modified MarketExtension/Assets/StoreLogo.scale-200.png
Binary file modified MarketExtension/Assets/StoreLogo.scale-400.png
Binary file modified MarketExtension/Assets/Wide310x150Logo.scale-100.png
Binary file modified MarketExtension/Assets/Wide310x150Logo.scale-125.png
Binary file modified MarketExtension/Assets/Wide310x150Logo.scale-150.png
Binary file modified MarketExtension/Assets/Wide310x150Logo.scale-200.png
Binary file modified MarketExtension/Assets/Wide310x150Logo.scale-400.png
95 changes: 95 additions & 0 deletions MarketExtension/Commands/MembershipCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using MarketExtension.Properties;

namespace MarketExtension;

// The watchlist/favorites membership actions. These now live solely on the per-symbol SymbolDetailPage
// (the single place to manage membership); list rows just navigate into it. Each one just mutates
// WatchlistStore for one instrument; the store re-publishes its observable subsets, so every live page,
// the dock, and the detail page's own command bar re-render themselves \u2014 no manual refresh callback is
// threaded through any more. It also pops a confirmation toast ("Added AAPL to watchlist") and keeps the
// palette open: the silent re-render alone left users unsure the action took, and KeepOpen lets them keep
// editing. Adapted from reference/commands/ToggleFavoriteCommand.cs.
internal abstract partial class MembershipCommand : InvokableCommand
{
// Segoe MDL2 glyphs: Add, Delete, FavoriteStar (outline), FavoriteStarFill.
protected const string AddGlyph = "\uE710";
protected const string DeleteGlyph = "\uE74D";
protected const string StarGlyph = "\uE734";
protected const string StarFillGlyph = "\uE735";

protected readonly DomainInstrument Instrument;

protected MembershipCommand(DomainInstrument instrument) => Instrument = instrument;

// Performs the mutation and returns the confirmation message to toast (so toggles can report
// exactly what they did).
protected abstract string Apply();

public override ICommandResult Invoke()
{
var message = Apply();
return CommandResult.ShowToast(new ToastArgs { Message = message, Result = CommandResult.KeepOpen() });
}
}

internal sealed partial class AddToWatchlistCommand : MembershipCommand
{
public AddToWatchlistCommand(DomainInstrument instrument) : base(instrument)
{
Name = Resources.Cmd_AddToWatchlist;
Icon = new IconInfo(AddGlyph);
}

protected override string Apply()
{
WatchlistStore.Instance.AddToWatchlist(Instrument);
return Strings.Format(Resources.Toast_AddedToWatchlist, Instrument.Symbol);
}
}

internal sealed partial class RemoveFromWatchlistCommand : MembershipCommand
{
public RemoveFromWatchlistCommand(DomainInstrument instrument) : base(instrument)
{
Name = Resources.Cmd_RemoveFromWatchlist;
Icon = new IconInfo(DeleteGlyph);
}

protected override string Apply()
{
WatchlistStore.Instance.RemoveFromWatchlist(Instrument.Symbol);
return Strings.Format(Resources.Toast_RemovedFromWatchlist, Instrument.Symbol);
}
}

internal sealed partial class AddToFavoritesCommand : MembershipCommand
{
public AddToFavoritesCommand(DomainInstrument instrument) : base(instrument)
{
Name = Resources.Cmd_AddToFavorites;
Icon = new IconInfo(StarGlyph);
}

protected override string Apply()
{
WatchlistStore.Instance.AddToFavorites(Instrument);
return Strings.Format(Resources.Toast_AddedToFavorites, Instrument.Symbol);
}
}

internal sealed partial class RemoveFromFavoritesCommand : MembershipCommand
{
public RemoveFromFavoritesCommand(DomainInstrument instrument) : base(instrument)
{
Name = Resources.Cmd_RemoveFromFavorites;
Icon = new IconInfo(StarFillGlyph);
}

protected override string Apply()
{
WatchlistStore.Instance.RemoveFromFavorites(Instrument.Symbol);
return Strings.Format(Resources.Toast_RemovedFromFavorites, Instrument.Symbol);
}
}
37 changes: 37 additions & 0 deletions MarketExtension/Commands/OpenUrlCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using MarketExtension.Properties;

namespace MarketExtension;

// Opens a URL in the user's default browser. Used by the Elbstream attribution row (see
// AssetIconResolver.AttributionRow). Keeps the palette open so the user returns to their list after the
// browser launches.
internal sealed partial class OpenUrlCommand : InvokableCommand
{
private readonly string _url;

public OpenUrlCommand(string url, string name)
{
_url = url;
Name = name;
}

public override ICommandResult Invoke()
{
try
{
ProcessHelper.OpenUrl(_url);
return CommandResult.KeepOpen();
}
catch (Exception ex)
{
return CommandResult.ShowToast(new ToastArgs
{
Message = Strings.Format(Resources.Toast_OpenUrlFailed, _url, ex.Message),
Result = CommandResult.KeepOpen(),
});
}
}
}
34 changes: 34 additions & 0 deletions MarketExtension/Commands/PortfolioCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using MarketExtension.Properties;

namespace MarketExtension;

// Removes a holding from the portfolio. Like the watchlist/favorites membership commands, it just
// mutates the store (PortfolioStore re-publishes its flows, so the Portfolio screen and the detail
// page's command bar re-render themselves) and pops a confirmation toast while keeping the palette open
// so the user knows it took and can keep editing. Adding/editing a holding is a page (SetQuantityPage),
// not a one-shot command, because it needs a quantity input.
internal sealed partial class RemoveFromPortfolioCommand : InvokableCommand
{
private const string DeleteGlyph = "\uE74D"; // Segoe MDL2 Delete

private readonly DomainInstrument _instrument;

public RemoveFromPortfolioCommand(DomainInstrument instrument)
{
_instrument = instrument;
Name = Resources.Cmd_RemoveFromPortfolio;
Icon = new IconInfo(DeleteGlyph);
}

public override ICommandResult Invoke()
{
PortfolioStore.Instance.Remove(_instrument.Symbol);
return CommandResult.ShowToast(new ToastArgs
{
Message = Strings.Format(Resources.Toast_RemovedFromPortfolio, _instrument.Symbol),
Result = CommandResult.KeepOpen(),
});
}
}
28 changes: 0 additions & 28 deletions MarketExtension/Commands/ToggleFavoriteCommand.cs

This file was deleted.

21 changes: 21 additions & 0 deletions MarketExtension/Data/Finnhub/ApiFinnhubCandleDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace MarketExtension;

// Api layer: the raw shape of Finnhub's GET /stock/candle (and /crypto/candle — identical shape)
// response. Internal to the Finnhub provider, which maps it into a DomainCandleSeries. The arrays are
// column-oriented and parallel by index: Close[i]/Timestamps[i] are candle i's close and time. Status
// is "ok" or "no_data". Open/High/Low/Volume are parsed but currently unused (kept for a future OHLC
// tooltip). Premium endpoint — a free-tier key gets a 403 (handled in the provider).
//
// Registered on FinnhubJsonContext via the SINGLE [JsonSerializable] declaration in
// ApiFinnhubQuoteDto.cs — never a second partial declaration (see the source-gen gotcha in CLAUDE.md).
internal sealed record ApiFinnhubCandleDto(
[property: JsonPropertyName("c")] IReadOnlyList<decimal>? Close,
[property: JsonPropertyName("h")] IReadOnlyList<decimal>? High,
[property: JsonPropertyName("l")] IReadOnlyList<decimal>? Low,
[property: JsonPropertyName("o")] IReadOnlyList<decimal>? Open,
[property: JsonPropertyName("t")] IReadOnlyList<long>? Timestamps,
[property: JsonPropertyName("v")] IReadOnlyList<decimal>? Volume,
[property: JsonPropertyName("s")] string? Status);
14 changes: 14 additions & 0 deletions MarketExtension/Data/Finnhub/ApiFinnhubProfileDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;

namespace MarketExtension;

// Api layer: the slice of Finnhub's GET /stock/profile2?symbol= response we care about — the instrument's
// reporting `currency`. Finnhub's /quote (c/d/dp/pc) carries NO currency, so the provider resolves it
// here, ONCE per symbol (currency is static metadata), and caches it forever. US tickers come back "USD";
// a paid plan's non-US listings report their local currency (e.g. "GBp" for London → normalized to GBP).
//
// Registered on FinnhubJsonContext in ApiFinnhubQuoteDto.cs (the single context declaration) per the
// CLAUDE.md source-gen gotcha — NOT in its own context here.
internal sealed record ApiFinnhubProfileDto(
[property: JsonPropertyName("currency")] string? Currency,
[property: JsonPropertyName("ticker")] string? Ticker);
21 changes: 21 additions & 0 deletions MarketExtension/Data/Finnhub/ApiFinnhubQuoteDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;

namespace MarketExtension;

// Api layer: the raw shape of Finnhub's GET /quote response. Internal to the Finnhub provider,
// which maps it into a DomainQuote. Fields are nullable because Finnhub returns null for
// change/percent on instruments without a previous close; the provider coalesces to 0.
//
// Source-generated (de)serialization keeps the AOT/trim build clean: reflection-based JSON would
// trip ILLinkTreatWarningsAsErrors.
internal sealed record ApiFinnhubQuoteDto(
[property: JsonPropertyName("c")] decimal? Current, // current price
[property: JsonPropertyName("d")] decimal? Change, // absolute change
[property: JsonPropertyName("dp")] decimal? ChangePercent, // percent change
[property: JsonPropertyName("pc")] decimal? PreviousClose); // previous close

[JsonSerializable(typeof(ApiFinnhubQuoteDto))]
[JsonSerializable(typeof(ApiFinnhubSearchDto))] // /search response — see ApiFinnhubSearchDto.cs
[JsonSerializable(typeof(ApiFinnhubCandleDto))] // /stock/candle + /crypto/candle — see ApiFinnhubCandleDto.cs
[JsonSerializable(typeof(ApiFinnhubProfileDto))] // /stock/profile2 (native currency) — see ApiFinnhubProfileDto.cs
internal sealed partial class FinnhubJsonContext : JsonSerializerContext;
24 changes: 24 additions & 0 deletions MarketExtension/Data/Finnhub/ApiFinnhubSearchDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace MarketExtension;

// Api layer: the raw shape of Finnhub's GET /search (symbol lookup) response. Internal to the
// Finnhub provider, which maps each result into a provider-agnostic DomainInstrument. All fields
// are nullable because Finnhub omits them for sparse matches; the provider skips results without a
// symbol.
//
// Source-generated (de)serialization keeps the AOT/trim build clean — reflection-based JSON would
// trip ILLinkTreatWarningsAsErrors. Registration lives on the single FinnhubJsonContext
// declaration in ApiFinnhubQuoteDto.cs: the JSON source generator does NOT support [JsonSerializable]
// attributes split across multiple partial declarations (it emits colliding hintNames and fails),
// so all serializable types for that context must be attributed in one place.
internal sealed record ApiFinnhubSearchDto(
[property: JsonPropertyName("count")] int Count,
[property: JsonPropertyName("result")] IReadOnlyList<ApiFinnhubSearchResultDto>? Result);

internal sealed record ApiFinnhubSearchResultDto(
[property: JsonPropertyName("description")] string? Description, // e.g. "APPLE INC"
[property: JsonPropertyName("displaySymbol")] string? DisplaySymbol, // UI symbol, e.g. "AAPL"
[property: JsonPropertyName("symbol")] string? Symbol, // canonical id for /quote
[property: JsonPropertyName("type")] string? Type); // e.g. "Common Stock"
Loading
Loading