Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# Default owners for the whole repo. Pair with branch protection:
# Settings → Branches → (rule) → "Require review from Code Owners".
# Team slug must match GitHub (org shows as syllogic-ai on github.com/syllogic-ai).
# Default owners for the whole repo.
#
# Branch protection: enable "Require review from Code Owners" on protected branches.
#
# If GitHub reports "Unknown owner" for @syllogic-ai/developers, fix org/repo settings:
# 1) Team visibility: Organization → Teams → developers must be Visible (not Secret).
# Secret teams cannot be used in CODEOWNERS.
# 2) Grant this team Write (or Maintain/Admin) on this repository:
# Repo → Settings → Collaborators and teams → Add teams → developers → Write.
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
#
* @syllogic-ai/developers
11 changes: 11 additions & 0 deletions backend/app/services/holding_valuation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,19 @@ def _price_for(self, h: Holding, on: date) -> tuple[Decimal, str, bool]:
if h.instrument_type == "cash":
return Decimal("1"), h.currency, False
lookup_symbol = h.provider_symbol or h.symbol

# First, try to fetch the price for the requested date (or most recent)
quotes = self.price_service.get_or_fetch([lookup_symbol], on)
if lookup_symbol in quotes:
quote = quotes[lookup_symbol]
gap_days = (on - quote.date).days
is_stale = gap_days > self.STALE_AFTER_DAYS
return quote.close, quote.currency, is_stale

# If fetch failed, fall back to latest snapshot in DB
snap = self.price_service.latest_snapshot(lookup_symbol, on)
if snap is None:
logger.warning("No price available for %s on %s", lookup_symbol, on)
return Decimal("0"), h.currency, True
gap_days = (on - snap.date).days
is_stale = gap_days > self.STALE_AFTER_DAYS
Expand Down
4 changes: 4 additions & 0 deletions deploy/compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ services:
DATA_ENCRYPTION_KEY_CURRENT: ${DATA_ENCRYPTION_KEY_CURRENT:-}
DATA_ENCRYPTION_KEY_PREVIOUS: ${DATA_ENCRYPTION_KEY_PREVIOUS:-}
DATA_ENCRYPTION_KEY_ID: ${DATA_ENCRYPTION_KEY_ID:-k1}
# Broker credential encryption
SYLLOGIC_SECRET_KEY: ${SYLLOGIC_SECRET_KEY:-}
# Optional integrations / features
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
depends_on:
Expand Down Expand Up @@ -122,6 +124,8 @@ services:
DATA_ENCRYPTION_KEY_CURRENT: ${DATA_ENCRYPTION_KEY_CURRENT:-}
DATA_ENCRYPTION_KEY_PREVIOUS: ${DATA_ENCRYPTION_KEY_PREVIOUS:-}
DATA_ENCRYPTION_KEY_ID: ${DATA_ENCRYPTION_KEY_ID:-k1}
# Broker credential encryption
SYLLOGIC_SECRET_KEY: ${SYLLOGIC_SECRET_KEY:-}
# Optional integrations / features
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
depends_on:
Expand Down
100 changes: 86 additions & 14 deletions frontend/components/investments/BrokerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import {
RiBankLine,
RiExternalLinkLine,
RiEyeLine,
RiEyeOffLine,
RiInformationLine,
RiRefreshLine,
} from "@remixicon/react";
import { createBrokerConnection } from "@/lib/api/investments";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
Expand All @@ -20,6 +27,76 @@ import {
} from "@/components/ui/select";
import { Field, Input } from "./_form-bits";

const SETUP_STEPS = [
{
step: 1,
title: "Open Flex Queries",
description:
'In IBKR Account Management, go to "Performance & Reports" → "Flex Queries".',
},
{
step: 2,
title: "Create Activity Flex Query",
description:
'Click the "+" next to Activity Flex Query. Give it a name (e.g. "Syllogic Positions"), then in Sections select "Open Positions" and "Cash Report". Set format to XML and save.',
},
{
step: 3,
title: "Create Trade Confirmation Flex Query",
description:
'Click the "+" next to Trade Confirmation Flex Query. Name it (e.g. "Syllogic Trades"), select the "Trades" section, set format to XML and save.',
},
{
step: 4,
title: "Enable Flex Web Service",
description:
'At the top of the Flex Queries page, click the gear icon next to "Flex Web Service" and enable it. Copy the "Current Token" shown.',
},
{
step: 5,
title: "Copy Query IDs",
description:
"Note down the Query ID shown next to each Flex Query you created. You'll need the Activity Query ID and Trade Confirmation Query ID.",
},
];

function FlexQuerySetupGuide() {
return (
<Dialog>
<DialogTrigger className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors">
<RiInformationLine size={14} />
<span>How to set up Flex Queries</span>
</DialogTrigger>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Setting up IBKR Flex Queries</DialogTitle>
</DialogHeader>
<div className="space-y-4 mt-2">
{SETUP_STEPS.map(({ step, title, description }) => (
<div key={step} className="flex gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-medium flex items-center justify-center">
{step}
</div>
<div className="space-y-0.5">
<div className="text-sm font-medium">{title}</div>
<div className="text-xs text-muted-foreground leading-relaxed">
{description}
</div>
</div>
</div>
))}
<div className="pt-2 border-t border-border">
<p className="text-xs text-muted-foreground">
Once complete, paste your Flex Token and both Query IDs in the
form below.
</p>
</div>
</div>
</DialogContent>
</Dialog>
);
}

export function BrokerForm({ onCancel }: { onCancel: () => void }) {
const router = useRouter();
const [accountName, setAccountName] = useState("IBKR Main");
Expand Down Expand Up @@ -71,13 +148,16 @@ export function BrokerForm({ onCancel }: { onCancel: () => void }) {
</div>
</div>
<div className="bg-muted/40 border border-border px-4 py-3 space-y-2">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
What you need
<div className="flex items-center justify-between">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
What you need
</div>
<FlexQuerySetupGuide />
</div>
{[
"A Flex Web Service token — from IBKR Account Management → Reports → Flex Queries",
"A Positions Flex Query ID configured to export account positions",
"A Trades Flex Query ID configured to export trade history",
"Flex Web Service token",
"Activity Flex Query ID (with Open Positions)",
"Trade Confirmation Flex Query ID",
].map((t) => (
<div
key={t}
Expand All @@ -87,14 +167,6 @@ export function BrokerForm({ onCancel }: { onCancel: () => void }) {
<span>{t}</span>
</div>
))}
<a
href="https://www.interactivebrokers.com/en/index.php?f=1325"
target="_blank"
rel="noreferrer"
className="text-xs text-foreground mt-1 inline-flex items-center gap-1 hover:underline"
>
<RiExternalLinkLine size={11} /> How to set up Flex Queries →
</a>
</div>
<div className="space-y-3.5">
<div className="flex gap-3">
Expand Down
Loading