Automatischer Push: 32 Datei(en) geändert
- .claude/agents/executer.md - .claude/agents/planer.md - .claude/agents/pruefer.md - .claude/agents/recherche.md - .claude/skills/plan-executer-pruefung/SKILL.md - .claude/workflows/executer-welle.js - .claude/workflows/plankritik.js - .claude/workflows/pruefung.js - .claude/workflows/welle-mit-pruefung.js - .gitignore - .vault-tools/regelwerk-check/ENTWURF.md - .vault-tools/regelwerk-check/regelwerk_check.py - … und 20 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
d7ffc9a5b5
commit
67d674cd0b
32 files changed
+1618
-88
No files matched your search
@@ -27,6 +27,27 @@ Geschrieben am 2026-08-30 **vor** der ersten Zeile Code, wie es [[AI/Regelwerk/p
|
||||
|
||||
→ **Aufgelöst durch eine erzwungene Form.** Das Skript sucht das Literal `docs/plans/JJJJ-MM-TT` und klassifiziert das unmittelbar folgende Zeichen: `/` = Ordnerform, `.` = Dateiform. Regel: **Wer die Dateiform nennt, ohne die Ordnerform zu nennen, ist ein Befund** — denn `codeworkflow.md` schreibt beide vor und die Ordnerform ist die, die vergessen wird. Genau dieser Fall lag am 2026-08-30 in `planer.md` vor.
|
||||
|
||||
## Nachtrag 2026-09-23: Prüfung 5 prüft auch `effort:`
|
||||
|
||||
Geschrieben vor der Änderung am Skript. Anlass: Keine Agentendatei trug `effort:`, jeder Subagent erbte deshalb den Effort der Hauptsitzung, oft `xhigh` oder `max`. Seitdem steht der Effort je Rolle im Stamm und im Frontmatter (Messung und Begründung: [[AI/Regelwerk/codeworkflow|codeworkflow.md]], „Effort je Rolle"). Die Prüfung sorgt dafür, dass beide gleich bleiben.
|
||||
|
||||
| # | Frage | Herkunft | Auflösung |
|
||||
|---|---|---|---|
|
||||
| 5a | Welche Effort-Werte gibt es? | **Quelle** für die Stufen — Claude Code 2.1.280, Enum `["low","medium","high","xhigh","max"]` in der Programmdatei (nachgelesen 2026-09-23). Der Leser für Agentendateien nimmt darüber hinaus Großschreibung, `med` und ganze Zahlen an. **Projektvorgabe** ist die engere Form. | Erlaubt sind nur die fünf Wörter, klein geschrieben: eine Schreibweise, die jeder ohne Programmkenntnis liest. Die Meldung sagt deshalb „nicht in der Vorgabe", nicht „ungültig". Anführungszeichen um den Wert sind YAML und zählen nicht mit. |
|
||||
| 5b | Welcher Effort gilt je Agent? | **Projektvorgabe** — Rollentabelle im Stamm, Spalte mit der Überschrift „Effort", erster Backtick-Ausdruck der Zelle | Das `effort:` im Frontmatter muss ihm gleichen. Die Spalte wird über ihre Überschrift gefunden, nicht über die Position. |
|
||||
| 5c | Agentendatei ohne `effort:` oder mit leerem Wert | **Projektvorgabe** — Stamm: „Deshalb trägt jede Agentendatei `effort:`" | Befund: Der Agent erbt den Effort der Hauptsitzung. |
|
||||
| 5d | Rollentabelle ohne Effort-Spalte | **Projektvorgabe** | Befund am Stamm. Die Effort-Hälfte von Prüfung 5 schweigt dann nicht, sie meldet, dass sie nicht laufen kann. |
|
||||
| 5e | Tabellenzeile eines Agenten ohne Backtick-Wert in der Effort-Spalte | **Projektvorgabe** | Befund am Stamm, mit dem Namen des Agenten. |
|
||||
|
||||
Benannte Lücken, bewusst nicht geprüft:
|
||||
|
||||
- **`effort:` bei `model: haiku`.** Haiku 4.5 kennt keinen Effort; Claude Code sendet ihn dann nicht (Programmdatei). Die Rollentabelle führt keinen Agenten auf Haiku; die Abschrift-Stufe steht im Task-Frontmatter, nicht in einer Agentendatei.
|
||||
- **Doppelter Schlüssel `effort:`.** Der flache Frontmatter-Leser nimmt den letzten Wert, wie bei `model:`.
|
||||
- **Ob Claude Code den Wert anwendet.** Das zeigt nur das Feld `effort` im Transkript eines echten Laufs, und eine geänderte Agentendatei wirkt in der laufenden Sitzung nicht sofort. Belegt am 2026-09-23 in frischen Sitzungen über das Agent-Werkzeug und im Workflow.
|
||||
- **`effort` in gespeicherten Workflows unter `.claude/workflows/`.** Sie setzen den Effort mit `agentType` absichtlich nicht; eine Abweichung dort sieht diese Prüfung nicht.
|
||||
|
||||
Dazu zwei Korrekturen am Regressionslauf, die mit diesem Nachtrag gekommen sind: Die Grundlinie setzt `status: pending-review` in ihrer Kopie auf `reference`. Vorher brach der Lauf nach jeder Selbstpflege an der Grundlinie ab, weil Prüfung 4 dann anschlägt — gemessen am 2026-09-23. Und Prüfung 4 bekommt dafür erstmals einen eigenen Angriffsfall.
|
||||
|
||||
## Positivliste, nicht Negativliste
|
||||
|
||||
Alle sechs Prüfungen schränken **geschlossene** Mengen ein: die Menge der Wikilinks in einer Datei, die Menge der Pflichtfelder, die Menge der Agentendateien, die Menge der Regelwerk-Dateien. Es gibt keine Prüfung, die eine offene Menge verbotener Muster abzählt — und damit auch keine handgepflegte Negativliste, die gegen sie verlieren könnte.
|
||||
@@ -46,6 +67,16 @@ Je Fall eine Wegwerfkopie des Vaults, Rücksetzen dazwischen, Exit-Code und ausl
|
||||
| leer | Regelwerk-Datei auf 0 Byte gekürzt |
|
||||
| zusätzlich | neue Regelwerk-Datei, die niemand verlinkt |
|
||||
| Wertebereich | `model: sonnet` in `pruefer.md` (Tabelle sagt `opus`) |
|
||||
| Wertebereich | `effort: xhigh` in `pruefer.md` (Tabelle sagt `high`) |
|
||||
| Wertebereich | `effort: hoch` in `planer.md` — kein gültiger Wert |
|
||||
| fehlt, **Reihenfolge** | `effort:` fehlt in der ersten Agentendatei (`executer.md`) und, als eigener Fall, in der letzten (`recherche.md`) |
|
||||
| null | `effort:` ohne Wert in `planer.md` |
|
||||
| Umgebung | Effort-Spalte der Rollentabelle entfernt |
|
||||
| Verweis ins Leere | Effort-Zelle eines Agenten ohne Backtick-Wert |
|
||||
| Freigabe | eine Regelwerk-Datei auf `status: pending-review` |
|
||||
| Gegenfall (muss stumm bleiben) | `effort: "high"` in Anführungszeichen |
|
||||
| Gegenfall (muss stumm bleiben) | Zusatzspalte mit Backtick-Wert hinter der Effort-Spalte — fängt einen Rückfall auf die Spaltenposition |
|
||||
| Gegenfall (muss stumm bleiben) | Effort-Zelle mit zweitem Backtick-Wert — fängt „letzter statt erster Backtick-Ausdruck" |
|
||||
| Verweis ins Leere | Wikilink auf nicht existierende Datei |
|
||||
| Konvention | Dateiform der Planablage ohne Ordnerform |
|
||||
| **Reihenfolge** | toter Wikilink an **erster** und an **letzter** Position derselben Datei |
|
||||
@@ -57,7 +88,7 @@ Die letzten drei sind die wichtigsten: Sie prüfen, dass eine fehlende Vorausset
|
||||
|
||||
## Abbruchbedingung, vor der ersten Runde benannt
|
||||
|
||||
Schluss ist, wenn eine Runde nur noch Umformulierungen findet oder wenn alle verbleibenden Befunde dieselbe Antwort haben. Die Prüfung ist fertig, wenn alle dreizehn Fälle der Angriffsliste anschlagen und die Grundlinie grün ist — nicht, wenn sie „durchläuft".
|
||||
Schluss ist, wenn eine Runde nur noch Umformulierungen findet oder wenn alle verbleibenden Befunde dieselbe Antwort haben. Die Prüfung ist fertig, wenn alle Fälle der Angriffsliste anschlagen, alle Gegenfälle stumm bleiben und die Grundlinie grün ist — nicht, wenn sie „durchläuft". Die Zahl der Fälle zählt der Regressionslauf selbst; hier steht keine, weil sie bei jeder Erweiterung von Hand nachgezogen und dabei falsch würde.
|
||||
|
||||
## Was diese Prüfung ausdrücklich nicht ist
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Sechs Prüfungen, jede über eine GESCHLOSSENE Menge:
|
||||
2 Frontmatter Trägt jede Regelwerk-Datei die sieben Pflichtfelder?
|
||||
3 Erreichbarkeit Ist jede Regelwerk-Datei vom Stamm aus verlinkt?
|
||||
4 Freigabe Steht eine Regelwerk-Datei auf status: pending-review?
|
||||
5 Modell Weicht model: einer Agentendatei von der Rollentabelle ab?
|
||||
5 Modell/Effort Weichen model: oder effort: einer Agentendatei von der Rollentabelle ab?
|
||||
6 Planablage Nennt eine Datei die Dateiform ohne die Ordnerform?
|
||||
|
||||
Die Rollentabelle und die Liste der Regelwerk-Verweise werden AUS CLAUDE.md
|
||||
@@ -36,8 +36,12 @@ WAS DIESE PRÜFUNG NICHT SIEHT — benannte Lücken
|
||||
eine Heuristik im Positivlisten-Gewand. Bewusst weggelassen.
|
||||
|
||||
* Modellaussagen im Fließtext. "(`executer`, Sonnet — bei reiner Abschrift
|
||||
Haiku)" ist für einen Menschen eindeutig und für ein Skript nicht. Geprüft
|
||||
wird ausschließlich das model:-Feld im Frontmatter.
|
||||
Haiku)" ist für einen Menschen eindeutig und für ein Skript nicht.
|
||||
Geprüft werden ausschließlich die Felder model: und effort: im
|
||||
Frontmatter.
|
||||
|
||||
* Ein YAML-Kommentar hinter dem Wert (effort: high # …) wird nicht
|
||||
abgeschnitten und ergibt einen Befund; für model: galt das schon vorher.
|
||||
|
||||
* INHALTLICHE Widersprüche. Zwei Dateien können dieselbe Regel in
|
||||
verschiedenen Worten gegensätzlich formulieren, und diese Prüfung meldet
|
||||
@@ -46,6 +50,23 @@ WAS DIESE PRÜFUNG NICHT SIEHT — benannte Lücken
|
||||
aufgefallen: die Planablage (Prüfung 6) ja, "fortsetzen statt frisch
|
||||
starten" nein.
|
||||
|
||||
* effort: bei model: haiku. Haiku 4.5 kennt keinen Effort; Claude Code
|
||||
sendet ihn dann nicht (Claude Code 2.1.280, Programmdatei, nachgelesen
|
||||
2026-09-23). Die Rollentabelle führt keinen Agenten auf Haiku; die
|
||||
Abschrift-Stufe steht im Task-Frontmatter, nicht in einer Agentendatei.
|
||||
|
||||
* Doppelter Schlüssel effort:. Der flache Frontmatter-Leser nimmt den
|
||||
letzten Wert, wie bei model:.
|
||||
|
||||
* Ob Claude Code den Wert anwendet. Das zeigt nur das Feld effort im
|
||||
Transkript eines echten Laufs, und eine geänderte Agentendatei wirkt
|
||||
in der laufenden Sitzung nicht sofort. Belegt am 2026-09-23 in frischen
|
||||
Sitzungen über das Agent-Werkzeug und im Workflow.
|
||||
|
||||
* effort in gespeicherten Workflows unter .claude/workflows/. Sie setzen
|
||||
den Effort mit agentType absichtlich nicht; eine Abweichung dort sieht
|
||||
diese Prüfung nicht.
|
||||
|
||||
===============================================================================
|
||||
DIESES SKRIPT IST EIN FANGNETZ, KEIN NACHWEIS.
|
||||
===============================================================================
|
||||
@@ -103,6 +124,11 @@ def wikilinks(text: str) -> list[str]:
|
||||
PLANPFAD = re.compile(r"docs/plans/JJJJ-MM-TT[^\s`]*")
|
||||
BACKTICK = re.compile(r"`([^`]+)`")
|
||||
|
||||
# Die Stufen stammen aus Claude Code 2.1.280 (Programmdatei, nachgelesen
|
||||
# 2026-09-23). Claude Code selbst nimmt zusätzlich Großschreibung, "med"
|
||||
# und ganze Zahlen an; die Vorgabe hier ist bewusst enger.
|
||||
EFFORT_WERTE = ("low", "medium", "high", "xhigh", "max")
|
||||
|
||||
|
||||
class Befund:
|
||||
def __init__(self, schwere: str, ort: str, text: str) -> None:
|
||||
@@ -163,7 +189,7 @@ def rollentabelle(text: str) -> dict[str, str]:
|
||||
"""Liest die Zuordnung Agent -> Modellalias aus der Tabelle im Stamm.
|
||||
|
||||
Eine Zeile sieht so aus:
|
||||
| Planen, Spezifizieren | `planer` | Opus 5 (`opus`) |
|
||||
| Planen, Spezifizieren | `planer` | Opus 5.5 (`opus`) | `high` (Frontmatter) |
|
||||
Genommen wird der erste Backtick-Ausdruck der Agenten- und der
|
||||
Modellspalte. Zeilen ohne Backticks in der Agentenspalte (etwa
|
||||
'Hauptsitzung') werden übersprungen.
|
||||
@@ -184,6 +210,52 @@ def rollentabelle(text: str) -> dict[str, str]:
|
||||
return zuordnung
|
||||
|
||||
|
||||
def entquotet(wert: str) -> str:
|
||||
"""Nimmt passende einfache oder doppelte Anführungszeichen ab: 'high' -> high."""
|
||||
if len(wert) >= 2 and wert[0] == wert[-1] and wert[0] in ("'", '"'):
|
||||
return wert[1:-1]
|
||||
return wert
|
||||
|
||||
|
||||
def effort_spalte(text: str) -> int | None:
|
||||
"""Findet den Index der Effort-Spalte in der Rollentabelle im Stamm.
|
||||
|
||||
Gesucht wird die Tabellenzeile, deren Zellen (getrimmt) sowohl 'Agent'
|
||||
als auch 'Effort' enthalten — das ist die Kopfzeile. Der Index der Zelle
|
||||
'Effort' ist die Effort-Spalte.
|
||||
"""
|
||||
for zeile in text.split("\n"):
|
||||
if not zeile.startswith("|"):
|
||||
continue
|
||||
spalten = [s.strip() for s in zeile.strip("|").split("|")]
|
||||
if "Agent" in spalten and "Effort" in spalten:
|
||||
return spalten.index("Effort")
|
||||
return None
|
||||
|
||||
|
||||
def rollentabelle_effort(text: str, spalte: int) -> dict[str, str]:
|
||||
"""Liest den Effort je Agent aus der Rollentabelle im Stamm.
|
||||
|
||||
Genommen wird der erste Backtick-Ausdruck der Effort-Spalte (`spalte`).
|
||||
Fehlt ein Backtick-Ausdruck, ist der Wert für diesen Agenten leer.
|
||||
"""
|
||||
zuordnung: dict[str, str] = {}
|
||||
for zeile in text.split("\n"):
|
||||
if not zeile.startswith("|"):
|
||||
continue
|
||||
spalten = [s.strip() for s in zeile.strip("|").split("|")]
|
||||
if len(spalten) <= spalte:
|
||||
continue
|
||||
agenten = BACKTICK.findall(spalten[1])
|
||||
if not agenten:
|
||||
continue
|
||||
effort_treffer = BACKTICK.findall(spalten[spalte])
|
||||
wert = effort_treffer[0].strip() if effort_treffer else ""
|
||||
for name in agenten:
|
||||
zuordnung[name.strip()] = wert
|
||||
return zuordnung
|
||||
|
||||
|
||||
def stamm_regelwerkverweise(text: str) -> set[str]:
|
||||
"""Alle im Stamm verlinkten Regelwerk-Dateien, als Dateiname ohne Endung."""
|
||||
namen: set[str] = set()
|
||||
@@ -252,7 +324,13 @@ def pruefe_freigabe(vault: Path, befunde: list[Befund]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def pruefe_modelle(vault: Path, tabelle: dict[str, str], befunde: list[Befund]) -> None:
|
||||
def pruefe_modelle(
|
||||
vault: Path,
|
||||
tabelle: dict[str, str],
|
||||
effort_index: int | None,
|
||||
effort_tabelle: dict[str, str],
|
||||
befunde: list[Befund],
|
||||
) -> None:
|
||||
for datei in sorted((vault / AGENTEN).glob("*.md")):
|
||||
rel = datei.relative_to(vault).as_posix()
|
||||
fm = frontmatter(lies(datei))
|
||||
@@ -278,6 +356,47 @@ def pruefe_modelle(vault: Path, tabelle: dict[str, str], befunde: list[Befund])
|
||||
)
|
||||
)
|
||||
|
||||
# Effort: nur prüfbar, wenn die Effort-Spalte der Rollentabelle
|
||||
# gefunden wurde (siehe main() für den Befund am Stamm, falls nicht).
|
||||
if effort_index is None:
|
||||
continue
|
||||
soll = effort_tabelle.get(name, "")
|
||||
if not soll:
|
||||
befunde.append(
|
||||
Befund("wichtig", "CLAUDE.md", f"Rollentabelle nennt für '{name}' keinen Effort")
|
||||
)
|
||||
effort_roh = fm.get("effort")
|
||||
if not effort_roh:
|
||||
befunde.append(
|
||||
Befund(
|
||||
"wichtig",
|
||||
rel,
|
||||
"kein effort: im Frontmatter — der Agent erbt den Effort der Hauptsitzung",
|
||||
)
|
||||
)
|
||||
continue
|
||||
effort_wert = entquotet(effort_roh)
|
||||
if not effort_wert:
|
||||
befunde.append(
|
||||
Befund(
|
||||
"wichtig",
|
||||
rel,
|
||||
"kein effort: im Frontmatter — der Agent erbt den Effort der Hauptsitzung",
|
||||
)
|
||||
)
|
||||
elif effort_wert not in EFFORT_WERTE:
|
||||
befunde.append(
|
||||
Befund(
|
||||
"blockierend",
|
||||
rel,
|
||||
f"effort: {effort_wert} ist nicht in der Vorgabe (erlaubt: low, medium, high, xhigh, max)",
|
||||
)
|
||||
)
|
||||
elif soll and effort_wert != soll:
|
||||
befunde.append(
|
||||
Befund("blockierend", rel, f"effort: {effort_wert} — Rollentabelle im Stamm sagt {soll}")
|
||||
)
|
||||
|
||||
|
||||
def pruefe_planablage(vault: Path, dateien: list[Path], befunde: list[Befund]) -> None:
|
||||
for datei in dateien:
|
||||
@@ -339,6 +458,17 @@ def main() -> int:
|
||||
Befund("blockierend", "CLAUDE.md", "keine Regelwerk-Verweise gefunden — Prüfung 3 kann nicht laufen")
|
||||
)
|
||||
|
||||
effort_index = effort_spalte(stammtext) if tabelle else None
|
||||
if tabelle and effort_index is None:
|
||||
befunde.append(
|
||||
Befund(
|
||||
"blockierend",
|
||||
"CLAUDE.md",
|
||||
"Effort-Spalte der Rollentabelle nicht auffindbar — Prüfung 5 kann den Effort nicht prüfen",
|
||||
)
|
||||
)
|
||||
effort_tabelle = rollentabelle_effort(stammtext, effort_index) if effort_index is not None else {}
|
||||
|
||||
dateien = dateien_im_geltungsbereich(vault)
|
||||
|
||||
pruefe_wikilinks(vault, dateien, befunde); gelaufen += 1
|
||||
@@ -347,7 +477,7 @@ def main() -> int:
|
||||
pruefe_erreichbarkeit(vault, verlinkt, befunde); gelaufen += 1
|
||||
pruefe_freigabe(vault, befunde); gelaufen += 1
|
||||
if tabelle and (vault / AGENTEN).is_dir():
|
||||
pruefe_modelle(vault, tabelle, befunde); gelaufen += 1
|
||||
pruefe_modelle(vault, tabelle, effort_index, effort_tabelle, befunde); gelaufen += 1
|
||||
pruefe_planablage(vault, dateien, befunde); gelaufen += 1
|
||||
|
||||
except SystemExit:
|
||||
|
||||
@@ -6,7 +6,10 @@ Angriffsliste und Begründung: .vault-tools/regelwerk-check/ENTWURF.md
|
||||
|
||||
Der Treiber baut aus dem echten Vault einen Minimalvault, in dem die Prüfung
|
||||
sauber durchläuft — das ist die GRUNDLINIE. Läuft sie nicht grün, misst der
|
||||
Rest nichts und der Treiber bricht ab.
|
||||
Rest nichts und der Treiber bricht ab. Dazu normalisiert die Grundlinie jede
|
||||
Frontmatter-Zeile status: pending-review auf status: reference — ohne das
|
||||
bricht der Lauf nach jeder Selbstpflege an der Grundlinie ab, weil Prüfung 4
|
||||
dann anschlägt (gemessen am 2026-09-23).
|
||||
|
||||
Danach je Angriffsfall: frische Kopie der Grundlinie, genau eine Beschädigung,
|
||||
Prüfung fahren, Exit-Code und auslösende Meldung protokollieren. Ein Fall gilt
|
||||
@@ -64,6 +67,15 @@ def baue_grundlinie(ziel: Path) -> None:
|
||||
unter.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(skill, unter / "SKILL.md")
|
||||
|
||||
# jede Zeile status: pending-review auf status: reference normalisieren.
|
||||
# Ohne das bricht der Lauf nach jeder Selbstpflege an der Grundlinie ab,
|
||||
# weil Prüfung 4 (Freigabe) dann anschlägt — gemessen am 2026-09-23.
|
||||
for datei in (ziel / "AI" / "Regelwerk").glob("*.md"):
|
||||
t = datei.read_text(encoding="utf-8")
|
||||
neu = re.sub(r"^status: pending-review$", "status: reference", t, flags=re.M)
|
||||
if neu != t:
|
||||
datei.write_text(neu, encoding="utf-8")
|
||||
|
||||
# Jedes Linkziel außerhalb des Geltungsbereichs als leere Datei anlegen,
|
||||
# damit die Grundlinie nicht an fehlendem Umfeld scheitert.
|
||||
for datei in list(ziel.rglob("*.md")):
|
||||
@@ -158,6 +170,144 @@ def s_keine_rollentabelle(v: Path) -> None:
|
||||
f.write_text(re.sub(r"^\|.*$", "", t, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_wertebereich_xhigh(v: Path) -> None:
|
||||
f = v / ".claude" / "agents" / "pruefer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort: high$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "effort: xhigh", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_wertebereich_ungueltig(v: Path) -> None:
|
||||
f = v / ".claude" / "agents" / "planer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort: high$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "effort: hoch", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_fehlt_erste(v: Path) -> None:
|
||||
"""Reihenfolge-Fall: effort: fehlt in der ERSTEN Agentendatei (executer.md)."""
|
||||
f = v / ".claude" / "agents" / "executer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort:.*\n"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_fehlt_letzte(v: Path) -> None:
|
||||
"""Reihenfolge-Fall: derselbe Schaden in der LETZTEN Agentendatei (recherche.md)."""
|
||||
f = v / ".claude" / "agents" / "recherche.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort:.*\n"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_null(v: Path) -> None:
|
||||
f = v / ".claude" / "agents" / "planer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort:.*$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "effort:", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_spalte_fehlt(v: Path) -> None:
|
||||
"""Entfernt in der Rollentabelle bei der Kopfzeile und allen direkt
|
||||
folgenden Tabellenzeilen die letzte Zelle — die Effort-Spalte."""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
kopfzeilen = [
|
||||
i for i, z in enumerate(zeilen)
|
||||
if z.startswith("|") and "Agent" in z and "Effort" in z
|
||||
]
|
||||
assert len(kopfzeilen) == 1
|
||||
start = kopfzeilen[0]
|
||||
ende = start
|
||||
while ende < len(zeilen) and zeilen[ende].startswith("|"):
|
||||
ende += 1
|
||||
for i in range(start, ende):
|
||||
teile = zeilen[i].split("|")
|
||||
kern = teile[1:-1]
|
||||
if len(kern) < 2:
|
||||
continue
|
||||
zeilen[i] = "|" + "|".join(kern[:-1]) + "|"
|
||||
f.write_text("\n".join(zeilen), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_verweis_leer(v: Path) -> None:
|
||||
"""Entfernt in der Tabellenzeile mit `pruefer` die Backticks um 'high'."""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
treffer = [i for i, z in enumerate(zeilen) if z.startswith("|") and "`pruefer`" in z]
|
||||
assert len(treffer) == 1
|
||||
i = treffer[0]
|
||||
assert zeilen[i].count("`high`") == 1
|
||||
zeilen[i] = zeilen[i].replace("`high`", "high", 1)
|
||||
f.write_text("\n".join(zeilen), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_freigabe_pending(v: Path) -> None:
|
||||
f = v / "AI" / "Regelwerk" / "schreibstil.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^status:.*$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "status: pending-review", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def g_effort_anfuehrung(v: Path) -> None:
|
||||
"""Kein Schaden: effort in passenden Anführungszeichen ist gültiges YAML.
|
||||
|
||||
Muss stumm bleiben — Regressionslauf-Fall zu Punkt 4 der Effort-Prüfung
|
||||
(Prüfwerkzeuge.md, Abschnitt 5: neuer Code bekommt seinen eigenen Fall).
|
||||
"""
|
||||
f = v / ".claude" / "agents" / "pruefer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort: high$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, 'effort: "high"', t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def g_zusatzspalte(v: Path) -> None:
|
||||
"""Kein Schaden: eine Spalte hinter Effort mit eigenem Backtick-Wert.
|
||||
|
||||
Muss stumm bleiben — die Effort-Spalte wird über ihre Überschrift
|
||||
gefunden, nicht über die letzte Spalte.
|
||||
"""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
kopfzeilen = [
|
||||
i for i, z in enumerate(zeilen)
|
||||
if z.startswith("|") and "Agent" in z and "Effort" in z
|
||||
]
|
||||
assert len(kopfzeilen) == 1
|
||||
start = kopfzeilen[0]
|
||||
zeilen[start] = zeilen[start].rstrip() + " Notiz |"
|
||||
zeilen[start + 1] = zeilen[start + 1].rstrip() + " --- |"
|
||||
i = start + 2
|
||||
while i < len(zeilen) and zeilen[i].startswith("|"):
|
||||
zeilen[i] = zeilen[i].rstrip() + " `xhigh` |"
|
||||
i += 1
|
||||
f.write_text("\n".join(zeilen), encoding="utf-8")
|
||||
|
||||
|
||||
def g_zweiter_backtick(v: Path) -> None:
|
||||
"""Kein Schaden: ein zweiter Backtick-Ausdruck in der Effort-Zelle.
|
||||
|
||||
Muss stumm bleiben — genommen wird der erste Backtick-Ausdruck.
|
||||
"""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
treffer = [i for i, z in enumerate(zeilen) if z.startswith("|") and "`pruefer`" in z]
|
||||
assert len(treffer) == 1
|
||||
i = treffer[0]
|
||||
assert zeilen[i].count("`high` (Frontmatter)") == 1
|
||||
zeilen[i] = zeilen[i].replace(
|
||||
"`high` (Frontmatter)", "`high` (Frontmatter, nicht `xhigh`)", 1
|
||||
)
|
||||
f.write_text("\n".join(zeilen), encoding="utf-8")
|
||||
|
||||
|
||||
def g_link_im_codeblock(v: Path) -> None:
|
||||
"""Kein Schaden, sondern eine Falle: Wikilink-artiges IN Code.
|
||||
|
||||
@@ -187,6 +337,9 @@ def g_link_im_codeblock(v: Path) -> None:
|
||||
# seinen eigenen Fall (pruefwerkzeuge.md, Abschnitt 5).
|
||||
GEGENFAELLE = [
|
||||
("codeblock | Wikilink-artiges in Code und Backticks", g_link_im_codeblock),
|
||||
("anfuehrung | effort in Anführungszeichen", g_effort_anfuehrung),
|
||||
("zusatzspalte | Spalte mit Backtick-Wert hinter Effort", g_zusatzspalte),
|
||||
("zweiter-backtick | Effort-Zelle mit zweitem Backtick-Wert", g_zweiter_backtick),
|
||||
]
|
||||
|
||||
|
||||
@@ -203,6 +356,14 @@ FAELLE = [
|
||||
("umgebung | CLAUDE.md fehlt", s_kein_stamm, "fehlt — nichts prüfbar"),
|
||||
("umgebung | AI/Regelwerk fehlt", s_kein_regelwerk, "Verzeichnis fehlt"),
|
||||
("umgebung | Rollentabelle unauffindbar", s_keine_rollentabelle, "Rollentabelle nicht auffindbar"),
|
||||
("wertebereich| effort: xhigh statt high", s_effort_wertebereich_xhigh, "effort: xhigh — Rollentabelle im Stamm sagt high"),
|
||||
("wertebereich| effort: hoch (ungültig)", s_effort_wertebereich_ungueltig, "ist nicht in der Vorgabe"),
|
||||
("fehlt | effort fehlt, ERSTE Agentendatei", s_effort_fehlt_erste, ".claude/agents/executer.md — kein effort: im Frontmatter"),
|
||||
("fehlt | effort fehlt, LETZTE Agentendatei", s_effort_fehlt_letzte, ".claude/agents/recherche.md — kein effort: im Frontmatter"),
|
||||
("null | effort ohne Wert", s_effort_null, ".claude/agents/planer.md — kein effort: im Frontmatter"),
|
||||
("umgebung | Effort-Spalte fehlt", s_effort_spalte_fehlt, "Effort-Spalte der Rollentabelle nicht auffindbar"),
|
||||
("verweis | Effort-Zelle ohne Wert", s_effort_verweis_leer, "Rollentabelle nennt für 'pruefer' keinen Effort"),
|
||||
("freigabe | status: pending-review", s_effort_freigabe_pending, "Freigabe steht aus"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -92,3 +92,14 @@ Die Compose-Probe (Bauen aus dem echten Baum mit verankerten Ausschlüssen, Star
|
||||
Vor dem Browser-Lauf `npm run build` im Projekt, sonst dient das Backend eine alte Oberfläche aus. Die Datensatz- und Dateinamen in `browserlauf.py` und `pruef_teil3b.py` sind die des Bestands vom 2026-09-17; nach einem neuen Katalog oder neuen Protokollen nachziehen. Die Statustexte in `wiederaufbau.py` sind aus `Kopfzeile.tsx` abgeschrieben.
|
||||
|
||||
Die generischen Planwerkzeuge liegen unter `.vault-tools/plan-kopie/`.
|
||||
|
||||
## Geheimnisdatei seit Paket E2 (2026-09-23)
|
||||
|
||||
Die Werkzeuge nennen `config/secrets.toml` nicht mehr fest. Wer eine Projektklasse ruft, gibt `None` weiter (`from_secrets(None, …)`, `secrets_pfad=None`, `lies_ntfy_geheimnisse()`); wer die Datei selbst liest (`handabruf_kerzen.py`, `handelstag_auswertung.py`, `vorstart_pruefung.py`, `pruef_beobachtung_echt.py`), holt den Pfad mit `trading.geheimnisse.pfad()` und nennt im Fehlerfall `geheimnisse.beschreibung()`; `ha_einrichten.py` löst gegen den übergebenen Projektordner auf. Der Auflöser nimmt das Argument, dann `TRADING_GEHEIMNISSE`, dann `$CREDENTIALS_DIRECTORY/secrets.toml`, sonst `config/secrets.toml` relativ zum Arbeitsverzeichnis — **nach dem Umzug der Datei ins Linux-Dateisystem steht die Variable nur in einer Login-Shell** (`~/.profile`). Ein Werkzeug aus einer Skriptdatei über `wsl --exec bash` sieht sie nicht und meldet „fehlt": dann `export TRADING_GEHEIMNISSE=/home/claude/.config/trading-plattform/secrets.toml` im Skript setzen. `start_probe_w.py` und `start_probe_zugang.py` nehmen beide Variablen absichtlich aus ihrer Umgebung, damit eine Probe nie die echte Datei liest; `ha_aktiv_setzen.py` bekommt den Pfad als Argument.
|
||||
|
||||
## Paket M (2026-09-24): zehn Aktien in einem Runner
|
||||
|
||||
| Datei | Was sie tut | Aufruf |
|
||||
|---|---|---|
|
||||
| `handelstag_auswertung.py` | seit Paket M mit mehreren Instrumenten: Instrumente aus dem Protokollkopf; je Instrument Entscheidungen, Orders und die aus der Folge erwartete Zahl (Stufen, Versatz, Entscheidungen, Schlussbein, ohne geteilte Beine), erreichte Positionen und Takte mit frischer Kerze; groesste Zahl von Orders in einem Takt, Ereignisse je Art, Positionen je Instrument am Ende, BEFUND fuer jede nicht gefuellte Orderzeile; das Taktfeld orders gilt als Orders dieses Takts; `--broker` und `--kerzen` fragen alle Symbole des Kopfs, jede Order gegen die Kerze ihres Symbols | wie bisher: `python handelstag_auswertung.py <protokoll> [--broker] [--kerzen]` aus dem Projektordner |
|
||||
| `m_probeaufruf.py` | lesender Probeaufruf vor dem Einspielen: `GET /v2/assets` roh (Schluessel und Typen) und ueber `AlpacaBroker.anlage`, `trades/latest` mit allen Symbolen roh und ueber `letzte_kurse`, Kerzen gebuendelt gegen einzeln, Seitenverhalten mit kleiner Seite; Exit 0 nur, wenn alles wie angenommen ist und keine Aktie die Startpruefung einer Folge mit Short verfehlt | aus einem Arbeitsbaum mit Paket M in WSL: `python m_probeaufruf.py <handelstag> <kennung> ...` |
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Liest die Antwortformen dreier Alpaca-Endpunkte fuer Paket M (zehn Aktien). Nur lesend, keine Order.
|
||||
|
||||
Koordinator-Werkzeug, nicht Teil des Repos (entstanden 2026-09-24 nachts). Ausgegeben werden Schluessel,
|
||||
Typen, Seitenmarken und die Handelsfelder der Anlagen (tradable, shortable, easy_to_borrow, fractionable,
|
||||
marginable) -- keine Kontodaten, kein Schluessel. Endpunkte:
|
||||
GET /v2/assets/{symbol} (Handels-API, paper-api)
|
||||
GET /v2/stocks/trades/latest?symbols=... (Daten-API, feed=iex)
|
||||
GET /v2/stocks/bars?symbols=...&timeframe=1Min (Daten-API, feed=iex, Seitenverhalten ueber Symbole)
|
||||
Aufruf (WSL, TRADING_GEHEIMNISSE gesetzt): python alpaca_formen_lesen.py JJJJ-MM-TT SYMBOL [SYMBOL ...]
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading import geheimnisse # noqa: E402
|
||||
|
||||
HANDEL = "https://paper-api.alpaca.markets"
|
||||
DATEN = "https://data.alpaca.markets"
|
||||
|
||||
|
||||
def form(wert, tiefe=0):
|
||||
if isinstance(wert, dict):
|
||||
if tiefe > 1:
|
||||
return "{...}"
|
||||
return "{" + ", ".join(f"{k}: {form(v, tiefe + 1)}" for k, v in list(wert.items())[:14]) + "}"
|
||||
if isinstance(wert, list):
|
||||
return f"[{len(wert)} x {form(wert[0], tiefe + 1) if wert else '-'}]"
|
||||
return type(wert).__name__
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tag, symbole = sys.argv[1], sys.argv[2:]
|
||||
werte = tomllib.loads(geheimnisse.pfad().read_text(encoding="utf-8"))["alpaca"]
|
||||
s = requests.Session()
|
||||
s.trust_env = False
|
||||
s.headers.update({"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]})
|
||||
|
||||
print("== /v2/assets/{symbol}")
|
||||
for sym in symbole:
|
||||
a = s.get(f"{HANDEL}/v2/assets/{sym}", timeout=20)
|
||||
if a.status_code != 200:
|
||||
print(sym, "HTTP", a.status_code)
|
||||
continue
|
||||
j = a.json()
|
||||
felder = {k: j.get(k) for k in ("symbol", "exchange", "status", "tradable", "shortable", "easy_to_borrow", "fractionable", "marginable")}
|
||||
print(felder)
|
||||
print("Schluessel einer Anlage:", sorted(j.keys()) if symbole else "-")
|
||||
|
||||
print("== /v2/stocks/trades/latest (gebuendelt)")
|
||||
t = s.get(f"{DATEN}/v2/stocks/trades/latest", params={"symbols": ",".join(symbole), "feed": "iex"}, timeout=20)
|
||||
print("HTTP", t.status_code, "Form:", form(t.json()))
|
||||
trades = t.json().get("trades", {})
|
||||
print("Symbole mit Trade:", len(trades), "von", len(symbole), "; Felder eines Trades:", sorted(next(iter(trades.values())).keys()) if trades else "-")
|
||||
|
||||
print("== /v2/stocks/bars (gebuendelt, Seiten)")
|
||||
start, ende = f"{tag}T13:30:00Z", f"{tag}T20:00:00Z"
|
||||
seiten, gesamt, je_symbol, token = 0, 0, {}, None
|
||||
while True:
|
||||
p = {"symbols": ",".join(symbole), "timeframe": "1Min", "start": start, "end": ende, "feed": "iex", "limit": 10000, "adjustment": "raw"}
|
||||
if token:
|
||||
p["page_token"] = token
|
||||
b = s.get(f"{DATEN}/v2/stocks/bars", params=p, timeout=30)
|
||||
if b.status_code != 200:
|
||||
print("HTTP", b.status_code, b.text[:200])
|
||||
return 1
|
||||
j = b.json()
|
||||
seiten += 1
|
||||
if seiten == 1:
|
||||
print("Form erste Seite:", form(j))
|
||||
for sym, liste in (j.get("bars") or {}).items():
|
||||
je_symbol[sym] = je_symbol.get(sym, 0) + len(liste)
|
||||
gesamt += len(liste)
|
||||
token = j.get("next_page_token")
|
||||
if not token or seiten > 20:
|
||||
break
|
||||
print("Seiten:", seiten, "Balken gesamt:", gesamt)
|
||||
print("Balken je Symbol:", dict(sorted(je_symbol.items())))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -118,7 +118,13 @@ def main() -> int:
|
||||
sys.path.insert(0, str(ha_projekt / "src"))
|
||||
import ha_verbindung as hv
|
||||
|
||||
geheimnisse = trading / "config" / "secrets.toml"
|
||||
# Seit E2 (2026-09-23): der Aufloeser des Projekts entscheidet -- TRADING_GEHEIMNISSE, sonst
|
||||
# config/secrets.toml relativ zum uebergebenen Projektordner.
|
||||
sys.path.insert(0, str(trading))
|
||||
from trading import geheimnisse as aufloeser # noqa: E402
|
||||
|
||||
aufgeloest = aufloeser.pfad()
|
||||
geheimnisse = aufgeloest if aufgeloest.is_absolute() else trading / aufgeloest
|
||||
token = hv.lies_token()
|
||||
print(f"Token gelesen (Laenge {len(token)}), Instanz aus dem HA-Projekt, Aktion {aktion}")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ ob die Kerzen verzoegert kommen. Sinnvoll, solange gehandelt wird -- Vorboerse a
|
||||
Berliner Zeit (04:00 ET), regulaer ab 15:30 Uhr. Nachts zeigt es nur die letzte Kerze des
|
||||
Vortags; dann belegt es nur, dass Schluessel und Abruf funktionieren.
|
||||
|
||||
Liest config/secrets.toml aus dem Projektordner, gibt die Schluessel nie aus.
|
||||
Liest die Geheimnisdatei ueber den Aufloeser des Projekts (TRADING_GEHEIMNISSE, sonst config/secrets.toml), gibt die Schluessel nie aus.
|
||||
|
||||
Aufruf aus dem Projektordner (WSL, Venv):
|
||||
~/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/trading-plattform-pruefung/handabruf_kerzen.py [feed]
|
||||
@@ -21,15 +21,18 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
from trading import geheimnisse # noqa: E402 -- aus dem Projektordner, wie oben verlangt
|
||||
|
||||
DATEN_URL = "https://data.alpaca.markets"
|
||||
FRISCH_S = 120 # eine abgeschlossene Minute plus die Wartezeit des Runners
|
||||
|
||||
|
||||
def main() -> int:
|
||||
feed = sys.argv[1] if len(sys.argv) > 1 else "iex"
|
||||
secrets = Path("config/secrets.toml")
|
||||
if not secrets.exists():
|
||||
print(f"VORAUSSETZUNG FEHLT: {secrets} (aus dem Projektordner starten)")
|
||||
secrets = geheimnisse.pfad()
|
||||
if not secrets.is_file():
|
||||
print(f"VORAUSSETZUNG FEHLT: {geheimnisse.beschreibung()}")
|
||||
return 3
|
||||
werte = tomllib.loads(secrets.read_text(encoding="utf-8"))["alpaca"]
|
||||
kopf = {"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]}
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
"""Wertet einen Paper-Handelstag aus: Protokoll des Runners, Orders des Paper-Kontos, IEX-Kerzen des Tages.
|
||||
|
||||
Koordinator-Werkzeug zu Teil 10, nicht fuers Repo. Es misst und berichtet; ein Urteil faellt es nur an
|
||||
drei Stellen, und dort gegen Werte aus dem Protokollkopf (Projektvorgabe, config/paper.toml):
|
||||
vier Stellen, und dort gegen Werte aus dem Protokollkopf (Projektvorgabe, config/paper.toml):
|
||||
- Annahme bis Fuellung einer Order (filled_at - created_at laut Alpaca) gegen order_timeout_s -- nicht
|
||||
filled_at - submitted_at: Am 2026-09-21 lagen zwischen Annahme und Weitergabe 8,7 s, und der Runner wartet
|
||||
ab der Annahme. Beide Spannen werden ausgegeben; mehr als die Haelfte der Grenze ist ein Hinweis,
|
||||
- Schlussposition ungleich null,
|
||||
- Schlussposition ungleich null (seit Paket M auch je Instrument),
|
||||
- eine Orderzeile des Protokolls, die nicht filled ist,
|
||||
- eine Position beim Broker mit negativer Stueckzahl (Short): wird nur gemeldet, nicht bewertet.
|
||||
|
||||
Was es misst:
|
||||
1. Aus dem Protokoll: Zeilen je Art, Takte je Phase und Killswitch-Zustand, Anteil frischer Kerzen,
|
||||
erste Entscheidung, Orders, Ende. Dazu je Takt mit frischer Kerze die Spanne
|
||||
erste Entscheidung, Orders, Ereignisse, Ende. Dazu je Takt mit frischer Kerze die Spanne
|
||||
takt.zeit - (letzte_kerze + 60 s). Das ist eine OBERGRENZE fuer die Zeit, bis die Kerze abrufbar war:
|
||||
Der Runner wartet vor dem ersten Abruf kerze_verzoegerung_s und schreibt die Taktzeile erst am Ende
|
||||
des Takts. Den Versuch, bei dem die Kerze kam, schreibt das Protokoll nicht mit; die Spanne laesst ihn
|
||||
nur schaetzen. Takte mit Order sind getrennt ausgewiesen, weil dort die Orderabfrage in der Spanne steckt.
|
||||
2. Mit --broker (lesend, Paper-Konto): Orders des Tages mit submitted_at, filled_at, filled_avg_price;
|
||||
offene Positionen mit Vorzeichen der Stueckzahl. Kein schreibender Aufruf.
|
||||
3. Mit --kerzen (lesend): IEX-Minutenkerzen des Tages; je gefuellter Order der Abstand des Fuellkurses
|
||||
zum Schluss der letzten vor der Order abgeschlossenen Kerze, in Basispunkten, mit Vorzeichen so,
|
||||
dass positiv Kosten bedeutet (Kauf ueber dem Schluss, Verkauf darunter).
|
||||
Das Taktfeld orders ist die Zahl der Orders DIESES Takts (PaperSitzung._takt_ende), keine Summe.
|
||||
Seit Paket M (2026-09-24) mit mehreren Instrumenten: die Instrumente aus dem Kopf; je Instrument
|
||||
Entscheidungen, Orders, die aus der Folge erwartete Zahl, erreichte Positionen und Takte mit frischer
|
||||
Kerze; die groesste Zahl von Orders in einem Takt, die Takte mit Orders in der Schlussphase und die
|
||||
Positionen je Instrument am Ende.
|
||||
2. Mit --broker (lesend, Paper-Konto): Orders des Tages in allen Symbolen des Kopfs mit submitted_at,
|
||||
filled_at, filled_avg_price; offene Positionen mit Vorzeichen der Stueckzahl. Kein schreibender Aufruf.
|
||||
3. Mit --kerzen (lesend): IEX-Minutenkerzen des Tages fuer alle Symbole des Kopfs, ueber alle Seiten; je
|
||||
gefuellter Order der Abstand des Fuellkurses zum Schluss der letzten vor der Order abgeschlossenen Kerze
|
||||
IHRES Symbols, in Basispunkten, mit Vorzeichen so, dass positiv Kosten bedeutet (Kauf ueber dem
|
||||
Schluss, Verkauf darunter).
|
||||
Was es nicht sieht: Teilausfuehrungen einzeln (nur den Durchschnittskurs der Order), den konsolidierten
|
||||
Kurs (nur IEX), Gebuehren (das Paper-Konto rechnet keine), und ob ein ausgebliebener Takt am Runner oder
|
||||
am Rechner lag. Ein Fangnetz fuer die Auswertung, kein Nachweis ueber die Handelsidee.
|
||||
|
||||
Liest config/secrets.toml aus dem Projektordner und gibt die Schluessel nie aus.
|
||||
Liest die Geheimnisdatei ueber den Aufloeser des Projekts (TRADING_GEHEIMNISSE, sonst config/secrets.toml) und gibt die Schluessel nie aus.
|
||||
Aufruf aus dem Projektordner (WSL, Venv):
|
||||
python handelstag_auswertung.py <paper-protokoll.jsonl> [--broker] [--kerzen]
|
||||
Exit 0, 3 wenn eine Voraussetzung fehlt (Protokoll, Kopfzeile, Schluessel, Netz).
|
||||
@@ -38,14 +45,61 @@ from collections import Counter
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
from trading import geheimnisse # noqa: E402 -- aus dem Projektordner, wie oben verlangt
|
||||
|
||||
HANDEL_URL = "https://paper-api.alpaca.markets"
|
||||
DATEN_URL = "https://data.alpaca.markets"
|
||||
|
||||
# Seiten des Kerzenabrufs, hoechstens. Zehn Symbole mal 390 Minuten sind rund 3.900 Kerzen, eine Seite.
|
||||
MAX_SEITEN = 20
|
||||
|
||||
|
||||
def zeitpunkt(text: str) -> datetime:
|
||||
return datetime.fromisoformat(text.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def instrumente_aus(kopf: dict) -> list[str]:
|
||||
"""Die Instrumente des Laufs: instrumente seit Paket M, sonst instrument, sonst AAPL wie bisher."""
|
||||
liste = kopf.get("instrumente")
|
||||
if isinstance(liste, list) and liste:
|
||||
return [str(kennung) for kennung in liste]
|
||||
einzeln = kopf.get("instrument")
|
||||
return [str(einzeln)] if einzeln else ["AAPL.XNAS"]
|
||||
|
||||
|
||||
def symbole_aus(kopf: dict) -> list[str]:
|
||||
return [kennung.split(".")[0] for kennung in instrumente_aus(kopf)]
|
||||
|
||||
|
||||
def erwartete_orders(agent: object, index: int, entscheidungen: int) -> int | None:
|
||||
"""Die Orders, die eine Folge bei so vielen Entscheidungen stellt, samt Schlussbein.
|
||||
|
||||
Nur fuer quelle folge, sonst None. Der Folgezaehler des Instruments beginnt bei index mal
|
||||
versatz_takte und zaehlt je Entscheidung eins weiter -- in einer Minute ohne frische Kerze
|
||||
entscheidet das Instrument nicht, und der Zaehler steht. Angenommen ist ein flacher Start;
|
||||
ein Seitenwechsel sind zwei Beine; steht die Folge am Ende nicht auf null, kommt das
|
||||
Schlussbein dazu. Geteilte Beine (ueber max_notional_je_order) zaehlt die Rechnung nicht mit.
|
||||
"""
|
||||
if not isinstance(agent, dict) or agent.get("quelle") != "folge":
|
||||
return None
|
||||
stufen = agent.get("stufen")
|
||||
takte = agent.get("takte_je_stufe")
|
||||
if not isinstance(stufen, list) or not stufen or not isinstance(takte, int) or takte < 1:
|
||||
return None
|
||||
versatz = index * int(agent.get("versatz_takte") or 0)
|
||||
vorher = 0
|
||||
orders = 0
|
||||
for zaehler in range(versatz, versatz + entscheidungen):
|
||||
stufe = stufen[(zaehler // takte) % len(stufen)]
|
||||
if stufe != vorher:
|
||||
orders += 2 if stufe * vorher < 0 else 1
|
||||
vorher = stufe
|
||||
if vorher != 0:
|
||||
orders += 1
|
||||
return orders
|
||||
|
||||
|
||||
def lies_protokoll(pfad: Path) -> list[dict]:
|
||||
zeilen = []
|
||||
# Dateiiteration statt splitlines(): splitlines trennt auch an U+2028 (Lesson vom 2026-09-17).
|
||||
@@ -84,6 +138,8 @@ def teil_protokoll(zeilen: list[dict]) -> dict | None:
|
||||
f"kopf: modus {kopf.get('modus')}, agent {kopf.get('agent')}, feed {kopf.get('feed')}, "
|
||||
f"order_timeout_s {kopf.get('order_timeout_s')}, start {kopf.get('zeit')}"
|
||||
)
|
||||
instrumente = instrumente_aus(kopf)
|
||||
print(f"instrumente: {instrumente}")
|
||||
takte = [z for z in zeilen if z.get("art") == "takt"]
|
||||
print(f"takte: {len(takte)}, phasen {dict(Counter(t.get('phase') for t in takte))}")
|
||||
print(f"killswitch je takt: {dict(Counter(t.get('killswitch') for t in takte))}")
|
||||
@@ -94,12 +150,9 @@ def teil_protokoll(zeilen: list[dict]) -> dict | None:
|
||||
if ohne:
|
||||
print(f" takte bei offenem markt OHNE frische kerze: {len(ohne)}; erste fuenf: {[t.get('zeit') for t in ohne[:5]]}")
|
||||
|
||||
orderschritte = set()
|
||||
letzter_orders = 0
|
||||
for t in takte:
|
||||
if isinstance(t.get("orders"), int) and t["orders"] > letzter_orders:
|
||||
orderschritte.add(t.get("schritt"))
|
||||
letzter_orders = t["orders"]
|
||||
# orders in der Taktzeile sind die Orders DIESES Takts (self._orders - vorher), keine laufende
|
||||
# Summe. Bis Paket M las das Werkzeug sie als Summe und ordnete nur Takte mit wachsender Zahl zu.
|
||||
orderschritte = {t.get("schritt") for t in takte if isinstance(t.get("orders"), int) and t["orders"] > 0}
|
||||
ruhig, mit_order = [], []
|
||||
for t in takte:
|
||||
if not t.get("frische_kerze") or not t.get("letzte_kerze") or not t.get("zeit"):
|
||||
@@ -127,15 +180,62 @@ def teil_protokoll(zeilen: list[dict]) -> dict | None:
|
||||
)
|
||||
print(f"aktionen: {dict(Counter(t.get('aktion') for t in entscheidungen))}")
|
||||
print(f"positionen je takt: {dict(Counter(t.get('position') for t in takte))}")
|
||||
if len(instrumente) > 1:
|
||||
erreicht = {
|
||||
kennung: sorted(
|
||||
{
|
||||
t["positionen"][kennung]
|
||||
for t in takte
|
||||
if isinstance(t.get("positionen"), dict) and kennung in t["positionen"]
|
||||
}
|
||||
)
|
||||
for kennung in instrumente
|
||||
}
|
||||
print(f"positionen erreicht je instrument: {erreicht}")
|
||||
frisch_je = {
|
||||
kennung: sum(
|
||||
1 for t in takte if isinstance(t.get("frische_kerzen"), dict) and t["frische_kerzen"].get(kennung)
|
||||
)
|
||||
for kennung in instrumente
|
||||
}
|
||||
print(f"takte mit frischer kerze je instrument: {frisch_je}")
|
||||
entscheidungen_je = {
|
||||
kennung: sum(
|
||||
1 for t in takte if isinstance(t.get("aktionen"), dict) and t["aktionen"].get(kennung) is not None
|
||||
)
|
||||
for kennung in instrumente
|
||||
}
|
||||
else:
|
||||
entscheidungen_je = {instrumente[0]: len(entscheidungen)}
|
||||
orders_je_takt = [t.get("orders") for t in takte if isinstance(t.get("orders"), int)]
|
||||
print(f"groesste zahl von orders in einem takt: {max(orders_je_takt) if orders_je_takt else 0}")
|
||||
schluss_mit_order = [t for t in takte if t.get("phase") == "schliesst" and (t.get("orders") or 0) > 0]
|
||||
print(f"takte mit orders in der schlussphase: {len(schluss_mit_order)}")
|
||||
hinweise = Counter(t.get("hinweis") for t in takte if t.get("hinweis"))
|
||||
for text, zahl in hinweise.most_common(8):
|
||||
print(f" hinweis x{zahl}: {str(text)[:140]}")
|
||||
for z in zeilen:
|
||||
if z.get("art") == "ereignis":
|
||||
print(f"ereignis {z.get('zeit')}: {z.get('was')} | {str(z.get('text'))[:160]} | phase {z.get('phase')}")
|
||||
for z in zeilen:
|
||||
if z.get("art") == "order":
|
||||
print(f"order {z.get('zeit')}: {z.get('seite')} {z.get('menge')} gefuellt {z.get('gefuellt')} status {z.get('status')} id {z.get('id')}")
|
||||
ereignisse = [z for z in zeilen if z.get("art") == "ereignis"]
|
||||
print(f"ereignisse je art: {dict(sorted(Counter(z.get('was') for z in ereignisse).items()))}")
|
||||
for z in ereignisse:
|
||||
print(f"ereignis {z.get('zeit')}: {z.get('was')} | {str(z.get('text'))[:160]} | phase {z.get('phase')}")
|
||||
orderzeilen = [z for z in zeilen if z.get("art") == "order"]
|
||||
for z in orderzeilen:
|
||||
print(
|
||||
f"order {z.get('zeit')}: {z.get('instrument') or ''} {z.get('seite')} {z.get('menge')} "
|
||||
f"gefuellt {z.get('gefuellt')} status {z.get('status')} position danach {z.get('position_danach')} id {z.get('id')}"
|
||||
)
|
||||
if z.get("status") != "filled":
|
||||
print(f"BEFUND: Orderzeile nicht gefuellt ({z.get('instrument') or ''} {z.get('seite')} {z.get('menge')}, status {z.get('status')})")
|
||||
orders_je = Counter(z.get("instrument") for z in orderzeilen if z.get("instrument"))
|
||||
print(f"orders je instrument: {dict(sorted(orders_je.items()))}")
|
||||
if len(instrumente) == 1 and not orders_je:
|
||||
# Protokolle von vor 0.28.0 tragen kein Instrument in der Orderzeile.
|
||||
orders_je = Counter({instrumente[0]: len(orderzeilen)})
|
||||
print("je instrument (erwartet: aus stufen, versatz und entscheidungen, samt schlussbein, ohne geteilte beine):")
|
||||
for index, kennung in enumerate(instrumente):
|
||||
erwartet = erwartete_orders(kopf.get("agent"), index, entscheidungen_je.get(kennung, 0))
|
||||
text = "" if erwartet is None else f", erwartet {erwartet}"
|
||||
print(f" {kennung}: entscheidungen {entscheidungen_je.get(kennung, 0)}, orders {orders_je.get(kennung, 0)}{text}")
|
||||
enden = [z for z in zeilen if z.get("art") == "ende"]
|
||||
if not enden:
|
||||
print("BEFUND: keine Zeile 'ende' -- der Runner wurde nicht ordentlich beendet")
|
||||
@@ -146,13 +246,19 @@ def teil_protokoll(zeilen: list[dict]) -> dict | None:
|
||||
)
|
||||
if ende.get("position") not in (0, None):
|
||||
print("BEFUND: Schlussposition ungleich null")
|
||||
positionen = ende.get("positionen")
|
||||
if isinstance(positionen, dict):
|
||||
print(f" positionen am ende: {positionen}")
|
||||
offen = {kennung: menge for kennung, menge in positionen.items() if menge != 0}
|
||||
if offen:
|
||||
print(f"BEFUND: Schlussposition ungleich null in {offen}")
|
||||
return kopf
|
||||
|
||||
|
||||
def kopfzeilen() -> dict[str, str] | None:
|
||||
secrets = Path("config/secrets.toml")
|
||||
if not secrets.exists():
|
||||
print(f"VORAUSSETZUNG FEHLT: {secrets} (aus dem Projektordner starten)")
|
||||
secrets = geheimnisse.pfad()
|
||||
if not secrets.is_file():
|
||||
print(f"VORAUSSETZUNG FEHLT: {geheimnisse.beschreibung()}")
|
||||
return None
|
||||
werte = tomllib.loads(secrets.read_text(encoding="utf-8"))["alpaca"]
|
||||
return {"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]}
|
||||
@@ -163,7 +269,7 @@ def teil_broker(kopf: dict, tag_start: datetime, anmeldung: dict[str, str]) -> l
|
||||
|
||||
parameter = {
|
||||
"status": "all",
|
||||
"symbols": "AAPL",
|
||||
"symbols": ",".join(symbole_aus(kopf)),
|
||||
"after": tag_start.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"until": (tag_start + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"direction": "asc",
|
||||
@@ -176,6 +282,7 @@ def teil_broker(kopf: dict, tag_start: datetime, anmeldung: dict[str, str]) -> l
|
||||
orders = antwort.json()
|
||||
grenze = float(kopf.get("order_timeout_s") or 0)
|
||||
print(f"orders beim broker am {tag_start.date()}: {len(orders)} (order_timeout_s laut protokoll: {grenze})")
|
||||
print(f"orders beim broker je symbol: {dict(sorted(Counter(o.get('symbol') for o in orders).items()))}")
|
||||
for o in orders:
|
||||
erstellt = zeitpunkt(o["created_at"]) if o.get("created_at") else None
|
||||
eingereicht = zeitpunkt(o["submitted_at"]) if o.get("submitted_at") else None
|
||||
@@ -214,11 +321,11 @@ def teil_broker(kopf: dict, tag_start: datetime, anmeldung: dict[str, str]) -> l
|
||||
return orders
|
||||
|
||||
|
||||
def teil_kerzen(orders: list[dict], tag_start: datetime, feed: str, anmeldung: dict[str, str]) -> None:
|
||||
def teil_kerzen(orders: list[dict], tag_start: datetime, feed: str, anmeldung: dict[str, str], symbole: list[str]) -> None:
|
||||
import requests
|
||||
|
||||
parameter = {
|
||||
"symbols": "AAPL",
|
||||
"symbols": ",".join(symbole),
|
||||
"timeframe": "1Min",
|
||||
"start": tag_start.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"end": (tag_start + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
@@ -227,13 +334,21 @@ def teil_kerzen(orders: list[dict], tag_start: datetime, feed: str, anmeldung: d
|
||||
"limit": 10_000,
|
||||
"sort": "asc",
|
||||
}
|
||||
antwort = requests.get(f"{DATEN_URL}/v2/stocks/bars", headers=anmeldung, params=parameter, timeout=30)
|
||||
if antwort.status_code != 200:
|
||||
print(f"VORAUSSETZUNG FEHLT: /v2/stocks/bars antwortet {antwort.status_code}: {antwort.text[:200]}")
|
||||
return
|
||||
kerzen = (antwort.json().get("bars") or {}).get("AAPL") or []
|
||||
print(f"kerzen ({feed}) am {tag_start.date()}: {len(kerzen)}")
|
||||
nach_zeit = {zeitpunkt(k["t"]): k for k in kerzen}
|
||||
kerzen: dict[str, list[dict]] = {symbol: [] for symbol in symbole}
|
||||
for _seite in range(MAX_SEITEN):
|
||||
antwort = requests.get(f"{DATEN_URL}/v2/stocks/bars", headers=anmeldung, params=parameter, timeout=30)
|
||||
if antwort.status_code != 200:
|
||||
print(f"VORAUSSETZUNG FEHLT: /v2/stocks/bars antwortet {antwort.status_code}: {antwort.text[:200]}")
|
||||
return
|
||||
nutzlast = antwort.json()
|
||||
for symbol, liste in (nutzlast.get("bars") or {}).items():
|
||||
kerzen.setdefault(symbol, []).extend(liste or [])
|
||||
token = nutzlast.get("next_page_token")
|
||||
if not token:
|
||||
break
|
||||
parameter["page_token"] = token
|
||||
print(f"kerzen ({feed}) am {tag_start.date()}: {dict((symbol, len(liste)) for symbol, liste in kerzen.items())}")
|
||||
nach_zeit = {symbol: {zeitpunkt(k["t"]): k for k in liste} for symbol, liste in kerzen.items()}
|
||||
for o in orders:
|
||||
if not o.get("filled_at") or not o.get("filled_avg_price") or not o.get("submitted_at"):
|
||||
continue
|
||||
@@ -241,16 +356,16 @@ def teil_kerzen(orders: list[dict], tag_start: datetime, feed: str, anmeldung: d
|
||||
# Die letzte vor der Order ABGESCHLOSSENE Kerze beginnt zwei Minutenanfaenge vor der laufenden Minute
|
||||
# nur dann nicht, wenn die Order in derselben Minute liegt wie ihr Schluss: Minute abrunden, eine zurueck.
|
||||
entscheidungskerze = eingereicht.replace(second=0, microsecond=0) - timedelta(minutes=1)
|
||||
kerze = nach_zeit.get(entscheidungskerze)
|
||||
kerze = nach_zeit.get(o.get("symbol"), {}).get(entscheidungskerze)
|
||||
if kerze is None:
|
||||
print(f" {o.get('side')} {o.get('qty')}: keine Kerze um {entscheidungskerze.isoformat()} (IEX-Luecke)")
|
||||
print(f" {o.get('symbol')} {o.get('side')} {o.get('qty')}: keine Kerze um {entscheidungskerze.isoformat()} (IEX-Luecke)")
|
||||
continue
|
||||
schluss = float(kerze["c"])
|
||||
kurs = float(o["filled_avg_price"])
|
||||
vorzeichen = 1.0 if o.get("side") == "buy" else -1.0
|
||||
bp = vorzeichen * (kurs - schluss) / schluss * 10_000
|
||||
print(
|
||||
f" {o.get('side')} {o.get('qty')}: fuellkurs {kurs:.4f}, schluss der kerze {entscheidungskerze.strftime('%H:%M')} UTC {schluss:.4f}, "
|
||||
f" {o.get('symbol')} {o.get('side')} {o.get('qty')}: fuellkurs {kurs:.4f}, schluss der kerze {entscheidungskerze.strftime('%H:%M')} UTC {schluss:.4f}, "
|
||||
f"kosten {bp:+.2f} bp ({vorzeichen * (kurs - schluss) * float(o.get('filled_qty') or 0):+.2f} USD)"
|
||||
)
|
||||
|
||||
@@ -285,7 +400,7 @@ def main() -> int:
|
||||
orders = ergebnis
|
||||
if "--kerzen" in schalter:
|
||||
print("== ausfuehrungskosten gegen die IEX-kerze")
|
||||
teil_kerzen(orders, tag_start, str(kopf.get("feed") or "iex"), anmeldung)
|
||||
teil_kerzen(orders, tag_start, str(kopf.get("feed") or "iex"), anmeldung, symbole_aus(kopf))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def main() -> int:
|
||||
datum, ziel = sys.argv[1], Path(sys.argv[2])
|
||||
config = lade_paper_config("config/paper.toml")
|
||||
mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
|
||||
quelle = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed=config.feed, timeout=20.0)
|
||||
quelle = AlpacaKerzenquelle.from_secrets(None, feed=config.feed, timeout=20.0)
|
||||
kerzen = quelle.kerzen_des_tages(config.instrument_id, handelsende(mitternacht))
|
||||
ziel.write_text(
|
||||
json.dumps(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Misst Luecken im IEX-Minutenfeed je Symbol und Handelstag. Nur lesend.
|
||||
|
||||
Koordinator-Werkzeug (2026-09-24 nachts, Paket M). Anlass: Der Gratis-Feed IEX liefert fuer weniger bei IEX
|
||||
gehandelte Werte nicht jede Minute eine Kerze (am 2026-09-23 AMGN 287 von 390). Der Watchdog haelt an, wenn
|
||||
der letzte Kurs aelter als kursstrom_max_alter_s (300 s) ist; diese Messung zeigt, wie lang die laengste
|
||||
Minute ohne Kerze je Symbol ist, und wie oft eine Luecke ueber 5 Minuten reicht. Ausgegeben werden nur Zaehler.
|
||||
Aufruf (WSL, TRADING_GEHEIMNISSE gesetzt): python iex_luecken.py TAG[,TAG...] SYMBOL [SYMBOL ...]
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading import geheimnisse # noqa: E402
|
||||
|
||||
DATEN = "https://data.alpaca.markets"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tage, symbole = sys.argv[1].split(","), sys.argv[2:]
|
||||
werte = tomllib.loads(geheimnisse.pfad().read_text(encoding="utf-8"))["alpaca"]
|
||||
s = requests.Session()
|
||||
s.trust_env = False
|
||||
s.headers.update({"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]})
|
||||
for tag in tage:
|
||||
p = {"symbols": ",".join(symbole), "timeframe": "1Min", "start": f"{tag}T13:30:00Z",
|
||||
"end": f"{tag}T20:00:00Z", "feed": "iex", "limit": 10000, "adjustment": "raw"}
|
||||
j = s.get(f"{DATEN}/v2/stocks/bars", params=p, timeout=30).json()
|
||||
assert not j.get("next_page_token"), "mehr als eine Seite"
|
||||
print(f"== {tag}")
|
||||
for sym in symbole:
|
||||
zeiten = [datetime.fromisoformat(b["t"].replace("Z", "+00:00")) for b in (j.get("bars") or {}).get(sym, [])]
|
||||
luecken = [int((b - a).total_seconds() // 60) - 1 for a, b in zip(zeiten, zeiten[1:])]
|
||||
ueber5 = sum(1 for x in luecken if x >= 5)
|
||||
print(f"{sym:5} Kerzen {len(zeiten):3} laengste Luecke {max(luecken) if luecken else '-':>3} min Luecken >=5 min: {ueber5}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Prüft für eine Liste von Symbolen den letzten Tagesschluss gegen eine Kursgrenze. Nur lesend.
|
||||
|
||||
Koordinator-Werkzeug, nicht Teil des Repos. Anlass: Paket M (zehn Aktien, 2026-09-25) — eine Order
|
||||
darf höchstens max_notional_je_order (10.000 USD) haben, ein Bein ist basis_menge (10) Stück, also
|
||||
muss jeder Kurs unter 1.000 USD liegen. Einziger Netzzugriff: ein lesender Abruf der Tageskerzen bei
|
||||
Alpaca (IEX). Ausgegeben werden Symbol, Datum der Kerze, Schlusskurs (öffentlicher Börsenkurs, kein
|
||||
Depotwert) und ob er unter der Grenze liegt; nie ein Schlüssel.
|
||||
Aufruf (WSL, aus dem Projektordner, TRADING_GEHEIMNISSE gesetzt):
|
||||
python kurs_unter_grenze.py 1000 AAPL MSFT NVDA ...
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading import geheimnisse # noqa: E402
|
||||
|
||||
DATEN_URL = "https://data.alpaca.markets"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
grenze = float(sys.argv[1])
|
||||
symbole = sys.argv[2:]
|
||||
werte = tomllib.loads(geheimnisse.pfad().read_text(encoding="utf-8"))["alpaca"]
|
||||
sitzung = requests.Session()
|
||||
sitzung.trust_env = False
|
||||
antwort = sitzung.get(
|
||||
f"{DATEN_URL}/v2/stocks/bars/latest",
|
||||
params={"symbols": ",".join(symbole), "feed": "iex"},
|
||||
headers={"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]},
|
||||
timeout=20,
|
||||
)
|
||||
print("HTTP", antwort.status_code)
|
||||
if antwort.status_code != 200:
|
||||
print(antwort.text[:300])
|
||||
return 1
|
||||
bars = antwort.json().get("bars", {})
|
||||
fehler = 0
|
||||
for symbol in symbole:
|
||||
bar = bars.get(symbol)
|
||||
if not bar:
|
||||
print(f"{symbol:6} keine Kerze")
|
||||
fehler += 1
|
||||
continue
|
||||
schluss = float(bar["c"])
|
||||
ok = schluss < grenze
|
||||
fehler += 0 if ok else 1
|
||||
print(f"{symbol:6} {bar['t'][:16]} {schluss:9.2f} {'unter' if ok else 'UEBER'} {grenze:.0f}")
|
||||
return 0 if fehler == 0 else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Lesender Probeaufruf fuer Paket M: GET /v2/assets, trades/latest und bars mit mehreren Symbolen.
|
||||
|
||||
Koordinator-Werkzeug, nicht Teil des Repos. Anlass: Paket M (zehn Aktien, 2026-09-25) haengt an drei
|
||||
Antwortformen, die im Projekt nie gemessen wurden -- GET /v2/assets/{symbol}, GET /v2/stocks/trades/latest
|
||||
mit mehreren Symbolen und das Seitenverhalten von GET /v2/stocks/bars mit mehreren Symbolen. Das Werkzeug
|
||||
belegt sie an genau dem Code, der sie liest (AlpacaBroker.anlage, AlpacaBroker.letzte_kurse,
|
||||
AlpacaKerzenquelle.kerzen_mehrerer), und zusaetzlich roh: Schluessel und Typen, nie Werte ausser den
|
||||
Wahrheitswerten der Startpruefung, Zaehlungen und dem Alter des letzten Trades.
|
||||
Nur GET, kein schreibender Aufruf. Liest die Geheimnisdatei ueber den Aufloeser des Projekts
|
||||
(TRADING_GEHEIMNISSE, sonst config/secrets.toml) und gibt die Schluessel nie aus.
|
||||
Aufruf (WSL, aus einem Arbeitsbaum mit Paket M, Venv):
|
||||
python m_probeaufruf.py <handelstag JJJJ-MM-TT> <kennung> [<kennung> ...]
|
||||
Exit 0: alle Formen wie angenommen. 2: eine Abweichung oder ein Starthindernis. 1: Ausnahme.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
|
||||
from trading import geheimnisse # noqa: E402
|
||||
from trading.broker.alpaca import DATEN_URL # noqa: E402
|
||||
from trading.broker.alpaca import TRADING_URL # noqa: E402
|
||||
from trading.broker.alpaca import AlpacaBroker # noqa: E402
|
||||
from trading.broker.schnittstelle import zu_symbol # noqa: E402
|
||||
from trading.paper.kerzen import HANDELSZONE # noqa: E402
|
||||
from trading.paper.kerzen import AlpacaKerzenquelle # noqa: E402
|
||||
from trading.paper.kerzen import handelsbeginn # noqa: E402
|
||||
from trading.paper.kerzen import handelsende # noqa: E402
|
||||
|
||||
FEED = "iex"
|
||||
# Klein genug, dass zehn Symbole mal 390 Minuten mehrere Seiten brauchen.
|
||||
KLEINE_SEITE = 1000
|
||||
MAX_SEITEN = 50
|
||||
|
||||
|
||||
def typ(wert) -> str:
|
||||
return type(wert).__name__
|
||||
|
||||
|
||||
def iso_z(zeitpunkt: datetime) -> str:
|
||||
return zeitpunkt.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def zaehle_seiten(sitzung, kopf: dict, parameter: dict) -> tuple[dict[str, int], int, int]:
|
||||
"""Kerzen je Symbol ueber alle Seiten, Zahl der Seiten, Seiten mit mehr als einem Symbol."""
|
||||
parameter = dict(parameter)
|
||||
je_symbol: dict[str, int] = {}
|
||||
seiten = 0
|
||||
gemischt = 0
|
||||
while seiten < MAX_SEITEN:
|
||||
antwort = sitzung.get(f"{DATEN_URL}/v2/stocks/bars", headers=kopf, params=parameter, timeout=30)
|
||||
seiten += 1
|
||||
if antwort.status_code != 200:
|
||||
raise RuntimeError(f"GET /v2/stocks/bars antwortete {antwort.status_code}")
|
||||
nutzlast = antwort.json()
|
||||
bars = nutzlast.get("bars") or {}
|
||||
if len(bars) > 1:
|
||||
gemischt += 1
|
||||
for symbol, liste in bars.items():
|
||||
je_symbol[symbol] = je_symbol.get(symbol, 0) + len(liste or [])
|
||||
token = nutzlast.get("next_page_token")
|
||||
if not token:
|
||||
break
|
||||
parameter["page_token"] = token
|
||||
return je_symbol, seiten, gemischt
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__)
|
||||
return 1
|
||||
datum = sys.argv[1]
|
||||
kennungen = sys.argv[2:]
|
||||
symbole = [zu_symbol(kennung) for kennung in kennungen]
|
||||
werte = tomllib.loads(geheimnisse.pfad().read_text(encoding="utf-8"))["alpaca"]
|
||||
kopf = {"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]}
|
||||
sitzung = requests.Session()
|
||||
sitzung.trust_env = False
|
||||
abweichungen = 0
|
||||
|
||||
# 1. GET /v2/assets roh fuer das erste Symbol: nur Schluessel und Typen.
|
||||
antwort = sitzung.get(f"{TRADING_URL}/v2/assets/{symbole[0]}", headers=kopf, timeout=20)
|
||||
print(f"GET /v2/assets/{symbole[0]}: HTTP {antwort.status_code}")
|
||||
roh = antwort.json() if antwort.status_code == 200 else {}
|
||||
if isinstance(roh, dict):
|
||||
print(f" schluessel und typen: {dict((k, typ(v)) for k, v in sorted(roh.items()))}")
|
||||
for feld, erwartet in (("symbol", "str"), ("tradable", "bool"), ("shortable", "bool"), ("easy_to_borrow", "bool")):
|
||||
gefunden = typ(roh.get(feld)) if isinstance(roh, dict) and feld in roh else "fehlt"
|
||||
if gefunden != erwartet:
|
||||
print(f"ABWEICHUNG: Feld {feld} ist {gefunden}, erwartet {erwartet}")
|
||||
abweichungen += 1
|
||||
|
||||
# 2. anlage() je Instrument ueber den Code des Projekts.
|
||||
broker = AlpacaBroker(
|
||||
werte["key_id"], werte["secret_key"], universum=tuple(kennungen), feed=FEED, timeout=20.0
|
||||
)
|
||||
for kennung in kennungen:
|
||||
anlage = broker.anlage(kennung)
|
||||
print(
|
||||
f"anlage {kennung}: tradable {anlage.handelbar}, shortable {anlage.leerverkaufbar}, "
|
||||
f"easy_to_borrow {anlage.leicht_leihbar}"
|
||||
)
|
||||
if not (anlage.handelbar and anlage.leerverkaufbar and anlage.leicht_leihbar):
|
||||
print(f"STARTHINDERNIS: {kennung} haelt die Startpruefung einer Folge mit Short nicht")
|
||||
abweichungen += 1
|
||||
|
||||
# 3. trades/latest roh und ueber den Code.
|
||||
antwort = sitzung.get(
|
||||
f"{DATEN_URL}/v2/stocks/trades/latest",
|
||||
headers=kopf,
|
||||
params={"symbols": ",".join(symbole), "feed": FEED},
|
||||
timeout=20,
|
||||
)
|
||||
print(f"GET /v2/stocks/trades/latest mit {len(symbole)} Symbolen: HTTP {antwort.status_code}")
|
||||
roh = antwort.json() if antwort.status_code == 200 else {}
|
||||
trades = roh.get("trades") if isinstance(roh, dict) else None
|
||||
print(f" schluessel oben: {sorted(roh) if isinstance(roh, dict) else typ(roh)}, trades ist {typ(trades)}")
|
||||
if not isinstance(trades, dict) or sorted(trades) != sorted(symbole):
|
||||
gesehen = sorted(trades) if isinstance(trades, dict) else typ(trades)
|
||||
print(f"ABWEICHUNG: trades traegt {gesehen} statt {sorted(symbole)}")
|
||||
abweichungen += 1
|
||||
kurse = broker.letzte_kurse(kennungen)
|
||||
jetzt = datetime.now(UTC)
|
||||
for kennung in kennungen:
|
||||
kurs = kurse.get(kennung)
|
||||
alter = "keiner" if kurs is None else f"vor {(jetzt - kurs.zeit).total_seconds():.0f} s"
|
||||
print(f"letzter trade {kennung}: {alter}")
|
||||
|
||||
# 4. Kerzen des Handelstags: gebuendelt ueber den Code gegen einzeln ueber den Code.
|
||||
mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
|
||||
bis = handelsende(mitternacht)
|
||||
quelle = AlpacaKerzenquelle(werte["key_id"], werte["secret_key"], feed=FEED, timeout=30.0)
|
||||
gebuendelt = quelle.kerzen_mehrerer(kennungen, bis)
|
||||
for kennung in kennungen:
|
||||
einzeln = quelle.kerzen_des_tages(kennung, bis)
|
||||
gleich = [k.zeit for k in einzeln] == [k.zeit for k in gebuendelt[kennung]]
|
||||
print(
|
||||
f"kerzen {kennung}: gebuendelt {len(gebuendelt[kennung])}, einzeln {len(einzeln)}"
|
||||
+ ("" if gleich else " ABWEICHUNG")
|
||||
)
|
||||
if not gleich:
|
||||
abweichungen += 1
|
||||
|
||||
# 5. Seitenverhalten roh: eine grosse Seite gegen viele kleine, je Symbol dieselbe Summe.
|
||||
grundparameter = {
|
||||
"symbols": ",".join(symbole),
|
||||
"timeframe": "1Min",
|
||||
"start": iso_z(handelsbeginn(bis)),
|
||||
"end": iso_z(bis),
|
||||
"feed": FEED,
|
||||
"adjustment": "raw",
|
||||
}
|
||||
gross, seiten_gross, _ = zaehle_seiten(sitzung, kopf, grundparameter | {"limit": 10_000})
|
||||
klein, seiten_klein, gemischt = zaehle_seiten(sitzung, kopf, grundparameter | {"limit": KLEINE_SEITE})
|
||||
summe = sum(gross.values())
|
||||
print(f"seiten mit limit 10000: {seiten_gross}, kerzen zusammen {summe}")
|
||||
print(f"seiten mit limit {KLEINE_SEITE}: {seiten_klein}, davon mit mehr als einem symbol: {gemischt}")
|
||||
if gross != klein:
|
||||
print(f"ABWEICHUNG: je Symbol ueber die kleinen Seiten {klein}, ueber die grosse {gross}")
|
||||
abweichungen += 1
|
||||
if summe > KLEINE_SEITE and seiten_klein == 1:
|
||||
print("HINWEIS: limit gilt offenbar je Symbol und nicht ueber alle; der Code sammelt in beiden Faellen richtig")
|
||||
|
||||
if abweichungen:
|
||||
print(f"ERGEBNIS: {abweichungen} Abweichung(en) oder Starthindernis(se)")
|
||||
return 2
|
||||
print("ERGEBNIS: alle Formen wie angenommen")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception as ausnahme: # noqa: BLE001 - ein Absturz ist selbst ein Befund
|
||||
print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}")
|
||||
sys.exit(1)
|
||||
@@ -18,8 +18,8 @@ from trading.paper.melder import lies_ntfy_geheimnisse
|
||||
|
||||
|
||||
def main() -> int:
|
||||
config = lade_melder_config("config/paper.toml", secrets_pfad="config/secrets.toml")
|
||||
thema, token = lies_ntfy_geheimnisse("config/secrets.toml")
|
||||
config = lade_melder_config("config/paper.toml", secrets_pfad=None)
|
||||
thema, token = lies_ntfy_geheimnisse()
|
||||
kopf = {"Authorization": f"Bearer {token}"} if token else {}
|
||||
try:
|
||||
antwort = requests.get(
|
||||
|
||||
@@ -105,7 +105,7 @@ def main():
|
||||
mitternacht = datetime.strptime(datum, "%Y-%m-%d").replace(tzinfo=HANDELSZONE)
|
||||
config = lade_paper_config("config/paper.toml")
|
||||
killswitch_config = lade_killswitch_config("config/killswitch.toml")
|
||||
quelle = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed=config.feed, timeout=10.0)
|
||||
quelle = AlpacaKerzenquelle.from_secrets(None, feed=config.feed, timeout=10.0)
|
||||
kerzen = list(quelle.kerzen_des_tages(config.instrument_id, handelsende(mitternacht)))
|
||||
print(f"{datum}: {len(kerzen)} IEX-Kerzen in {config.instrument_id}")
|
||||
schluesse = [k.close for k in kerzen]
|
||||
|
||||
@@ -39,6 +39,7 @@ import requests
|
||||
from nautilus_trader.model.data import BarType
|
||||
from nautilus_trader.test_kit.providers import TestInstrumentProvider
|
||||
|
||||
from trading import geheimnisse
|
||||
from trading.data.loaders import alpaca_json_zu_bars
|
||||
from trading.data.loaders import nur_regulaere_handelszeit
|
||||
from trading.paper.beobachtung import baue_beobachtung
|
||||
@@ -68,7 +69,7 @@ def main() -> int:
|
||||
ende = handelsende(tag)
|
||||
|
||||
# Weg des Runners.
|
||||
kerzen = AlpacaKerzenquelle.from_secrets("config/secrets.toml", feed="iex").kerzen_des_tages(
|
||||
kerzen = AlpacaKerzenquelle.from_secrets(None, feed="iex").kerzen_des_tages(
|
||||
"AAPL.XNAS", ende
|
||||
)
|
||||
print("Kerzen ueber die Kerzenquelle:", len(kerzen))
|
||||
@@ -77,7 +78,7 @@ def main() -> int:
|
||||
return 3
|
||||
|
||||
# Weg des Trainings: dieselbe Anfrage noch einmal roh, dann die Umwandlung des Katalogs.
|
||||
geheim = tomllib.loads(Path("config/secrets.toml").read_text(encoding="utf-8"))["alpaca"]
|
||||
geheim = tomllib.loads(geheimnisse.pfad().read_text(encoding="utf-8"))["alpaca"]
|
||||
kopf = {"APCA-API-KEY-ID": geheim["key_id"], "APCA-API-SECRET-KEY": geheim["secret_key"]}
|
||||
parameter = {
|
||||
"symbols": "AAPL",
|
||||
|
||||
@@ -21,7 +21,7 @@ from trading.broker.alpaca import AlpacaBroker # noqa: E402
|
||||
from trading.risk.limits import lade_limits # noqa: E402
|
||||
|
||||
limits = lade_limits("config/risk.toml")
|
||||
broker = AlpacaBroker.from_secrets("config/secrets.toml", universum=limits.universum, feed="iex", timeout=10.0)
|
||||
broker = AlpacaBroker.from_secrets(None, universum=limits.universum, feed="iex", timeout=10.0)
|
||||
|
||||
|
||||
def form(objekt) -> str:
|
||||
|
||||
@@ -13,7 +13,7 @@ Was es nicht sieht: ob das Dashboard laeuft (das fragt der Koordinator unter Win
|
||||
IEX-Feed frisch liefert (dafuer handabruf_kerzen.py), und den Code-Stand (dafuer git im Projektordner).
|
||||
Eine fehlende Voraussetzung ist ein Befund (Exit 3), jede Abweichung ein Befund (Exit 2).
|
||||
|
||||
Liest config/secrets.toml aus dem Projektordner und gibt die Schluessel nie aus.
|
||||
Liest die Geheimnisdatei ueber den Aufloeser des Projekts (TRADING_GEHEIMNISSE, sonst config/secrets.toml) und gibt die Schluessel nie aus.
|
||||
Aufruf aus dem Projektordner (WSL, Venv): python vorstart_pruefung.py
|
||||
"""
|
||||
|
||||
@@ -27,6 +27,9 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
sys.path.insert(0, str(Path.cwd()))
|
||||
from trading import geheimnisse # noqa: E402 -- aus dem Projektordner, wie oben verlangt
|
||||
|
||||
HANDEL_URL = "https://paper-api.alpaca.markets"
|
||||
|
||||
|
||||
@@ -59,9 +62,9 @@ def main() -> int:
|
||||
if zeile and zeile[0] == "gesperrt":
|
||||
befunde.append("Killswitch ist gesperrt; Fortsetzen allein genuegt dann nicht (Freigeben im Dashboard)")
|
||||
|
||||
secrets = Path("config/secrets.toml")
|
||||
secrets = geheimnisse.pfad()
|
||||
if not secrets.is_file():
|
||||
print("VORAUSSETZUNG FEHLT: config/secrets.toml")
|
||||
print(f"VORAUSSETZUNG FEHLT: {geheimnisse.beschreibung()}")
|
||||
return 3
|
||||
werte = tomllib.loads(secrets.read_text(encoding="utf-8"))["alpaca"]
|
||||
kopf = {"APCA-API-KEY-ID": werte["key_id"], "APCA-API-SECRET-KEY": werte["secret_key"]}
|
||||
|
||||
Reference in new issue
Block a user