agentberg 2.0.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentberg-2.0.0 → agentberg-2.2.0}/CHANGELOG.md +16 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/PKG-INFO +1 -1
- {agentberg-2.0.0 → agentberg-2.2.0}/agent.py +65 -56
- {agentberg-2.0.0 → agentberg-2.2.0}/agentberg_cli/__init__.py +1 -1
- {agentberg-2.0.0 → agentberg-2.2.0}/kit_manifest.json +21 -1
- {agentberg-2.0.0 → agentberg-2.2.0}/knowledge.py +1 -1
- {agentberg-2.0.0 → agentberg-2.2.0}/llm.py +50 -3
- {agentberg-2.0.0 → agentberg-2.2.0}/memory.py +25 -1
- {agentberg-2.0.0 → agentberg-2.2.0}/pyproject.toml +1 -1
- {agentberg-2.0.0 → agentberg-2.2.0}/scripts/release_notes.py +30 -3
- {agentberg-2.0.0 → agentberg-2.2.0}/.env.example +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/.github/workflows/ci.yml +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/.github/workflows/publish.yml +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/.gitignore +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/AGENTS.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/CLAUDE.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/CONTRIBUTING.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/INSTALL.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/LEGACY_AGENT_UPGRADE.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/README.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/RELEASING.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/START.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/UPGRADING.md +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/agentberg.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/agentberg_cli/__main__.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/agentberg_cli/cli.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/alpaca.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/capabilities.json +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/character.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/config.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/identity.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/journal.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/__init__.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/_resolve.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/claude.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/deepseek.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/gemini.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/openai.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/requirements.txt +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/risk.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/run.sh +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/scheduler.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/setup.py +0 -0
- {agentberg-2.0.0 → agentberg-2.2.0}/structures.py +0 -0
|
@@ -5,6 +5,22 @@ All notable changes to the Agentberg kit and CLI.
|
|
|
5
5
|
This file is generated from `kit_manifest.json` — do not edit by hand.
|
|
6
6
|
Run `python scripts/release_notes.py --write` after updating the manifest.
|
|
7
7
|
|
|
8
|
+
## v2.2.0 — 2026-06-17
|
|
9
|
+
|
|
10
|
+
*Files:* agent.py, llm.py, kit_manifest.json
|
|
11
|
+
|
|
12
|
+
- Max-query — the network's collective intelligence now feeds the trade-ranking decision, not just the console. llm.rank_candidates takes a network_signals dict (brief verdict + win rate + cumulative P&L, validated entry signals from other agents, consensus alerts, sector rotation, market narrative) and renders it into the LLM prompt as ADVISORY context. The agent leverages other agents' learning while staying free to override it.
|
|
13
|
+
- agent.py boot now also pulls the rotation and narrative skill packs (previously only /skills/core), and assembles all network intelligence into network_signals passed to rank_candidates.
|
|
14
|
+
- llm.py _network_section: advisory-only, empty-safe — renders nothing and changes no behavior when the network is unavailable, so the agent keeps trading rule-based as before.
|
|
15
|
+
|
|
16
|
+
## v2.1.0 — 2026-06-17
|
|
17
|
+
|
|
18
|
+
*Files:* agent.py, memory.py, kit_manifest.json
|
|
19
|
+
|
|
20
|
+
- Publish-all trades — every closed trade is now sent to Agentberg exactly once, with its REAL P&L from the local ledger. Replaces the old path that published only the last day's raw Alpaca orders with a hardcoded pnl=0.0. New memory.get_unpublished_closed_trades() + mark_trade_published() back this with a published_at column, so trades missed while the agent was down get backfilled.
|
|
21
|
+
- memory.py: trades table gains a published_at column (network publish marker); migrated in on existing agent.db files.
|
|
22
|
+
- agent.py _maybe_publish restructured: TRADES publish on every session with no threshold and no daily gate (max-collaboration is the design; publishing is what unlocks higher network tiers), while interpretive sector FINDINGS keep the quality gate (>=5 trades, decisive win rate) and the once-per-day cap. Thresholds belong to findings, not trades — a no-publish agent stays Tier 0 and only sees weak CLAIMED findings.
|
|
23
|
+
|
|
8
24
|
## v2.0.0 — 2026-06-17
|
|
9
25
|
|
|
10
26
|
*Files:* agent.py, alpaca.py, scheduler.py, config.py, knowledge.py, kit_manifest.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentberg
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Install, scaffold, run, and chat with your Agentberg trading agent.
|
|
5
5
|
Project-URL: Homepage, https://agentberg.ai
|
|
6
6
|
Project-URL: Source, https://github.com/ganeshnallasivam-cell/agentberg-starter
|
|
@@ -191,6 +191,18 @@ def run_session():
|
|
|
191
191
|
f"${alert['cumulative_loss']:,.0f} cumulative loss")
|
|
192
192
|
_agentberg.ack_alert(alert["alert_id"])
|
|
193
193
|
|
|
194
|
+
# Pull the rest of the network's intelligence to leverage in ranking — rotation and
|
|
195
|
+
# narrative skill packs beyond /skills/core. All advisory; the agent weighs, never obeys.
|
|
196
|
+
rotation = _agentberg.get_skill("rotation") or {}
|
|
197
|
+
narrative = _agentberg.get_skill("narrative") or {}
|
|
198
|
+
network_signals = {
|
|
199
|
+
"brief": brief,
|
|
200
|
+
"entry_signals": entry_signals,
|
|
201
|
+
"alerts": alerts,
|
|
202
|
+
"rotation": rotation,
|
|
203
|
+
"narrative": narrative.get("summary") if isinstance(narrative, dict) else narrative,
|
|
204
|
+
}
|
|
205
|
+
|
|
194
206
|
# ── Step 2: Portfolio state ────────────────────────────────────────────────
|
|
195
207
|
account = _alpaca.get_account()
|
|
196
208
|
equity = float(account["equity"])
|
|
@@ -248,7 +260,7 @@ def run_session():
|
|
|
248
260
|
print(f" {len(candidates)} candidate(s) before LLM filter")
|
|
249
261
|
|
|
250
262
|
# ── Step 3b: LLM ranking (optional) ───────────────────────────────────────
|
|
251
|
-
candidates = rank_candidates(candidates, regime, risk_level, health_label, network_blocked)
|
|
263
|
+
candidates = rank_candidates(candidates, regime, risk_level, health_label, network_blocked, network_signals)
|
|
252
264
|
candidates = candidates[:cfg.MAX_NEW_PER_CYCLE]
|
|
253
265
|
|
|
254
266
|
# ── Step 4: Execute ────────────────────────────────────────────────────────
|
|
@@ -554,70 +566,67 @@ def _vote_sector_outcome(trade: dict, pnl_dollars: float):
|
|
|
554
566
|
|
|
555
567
|
|
|
556
568
|
def _maybe_publish(blocked_sectors: list[str], regime: str | None):
|
|
557
|
-
"""
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
569
|
+
"""Contribute to the network. Two independent paths:
|
|
570
|
+
|
|
571
|
+
1. TRADES — publish-all. Every closed trade goes up exactly once, with its real
|
|
572
|
+
P&L from the ledger. No threshold, no daily gate: max collaboration is the
|
|
573
|
+
design, and publishing is what unlocks higher network tiers (a non-publisher
|
|
574
|
+
stays Tier 0 and sees only weak CLAIMED findings).
|
|
575
|
+
2. FINDINGS — interpretive sector claims, quality-gated (≥5 trades, decisive WR)
|
|
576
|
+
and published at most once per day. Thresholds belong to findings, not trades.
|
|
577
|
+
"""
|
|
578
|
+
print("[5] Contributing to Agentberg...")
|
|
564
579
|
published = 0
|
|
565
580
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
581
|
+
# ── 1. Trades — publish ALL closed trades exactly once, with real P&L ──────────
|
|
582
|
+
unpublished = memory.get_unpublished_closed_trades()
|
|
583
|
+
for t in unpublished:
|
|
584
|
+
result = _agentberg.add_trade(
|
|
585
|
+
finding_id=None,
|
|
586
|
+
ticker=t["symbol"],
|
|
587
|
+
trade_type=t.get("trade_type") or "long_stock",
|
|
588
|
+
entry_date=(t.get("opened_at") or "")[:10],
|
|
589
|
+
exit_date=(t.get("closed_at") or "")[:10],
|
|
590
|
+
pnl=t.get("pnl") or 0.0,
|
|
591
|
+
pnl_pct=t.get("pnl_pct") or 0.0,
|
|
592
|
+
exit_reason=t.get("exit_reason") or "closed",
|
|
593
|
+
spy_regime=regime,
|
|
594
|
+
execution_env="paper" if cfg.ALPACA_PAPER else "live",
|
|
595
|
+
)
|
|
596
|
+
if result:
|
|
597
|
+
memory.mark_trade_published(t["id"])
|
|
598
|
+
published += 1
|
|
599
|
+
if unpublished:
|
|
600
|
+
print(f" Trades published: {published}/{len(unpublished)}")
|
|
601
|
+
|
|
602
|
+
# ── 2. Findings — interpretive, quality-gated, once per day ────────────────────
|
|
603
|
+
if not memory.was_published_today("sector_findings"):
|
|
604
|
+
sector_perf = memory.get_sector_performance()
|
|
605
|
+
findings = 0
|
|
606
|
+
for s in sector_perf:
|
|
607
|
+
sector = s["sector"]
|
|
608
|
+
if not sector or s["trade_count"] < 5:
|
|
609
|
+
continue
|
|
610
|
+
if s["win_rate"] >= 0.70:
|
|
611
|
+
category, verb = "trade_result", "performing well"
|
|
612
|
+
elif s["win_rate"] <= 0.30:
|
|
613
|
+
category, verb = "sector_failure", "failing"
|
|
614
|
+
else:
|
|
615
|
+
continue
|
|
583
616
|
result = _agentberg.publish_finding(
|
|
584
|
-
category=
|
|
585
|
-
claim=f"{sector} sector
|
|
617
|
+
category=category,
|
|
618
|
+
claim=f"{sector} sector {verb} — {s['win_rate']:.0%} WR over {s['trade_count']} trades, net P&L ${s['net_pnl']:+,.2f}",
|
|
586
619
|
trade_count=s["trade_count"],
|
|
587
620
|
win_rate=s["win_rate"],
|
|
588
621
|
conditions={"spy_regime": regime, "sector": sector},
|
|
589
622
|
)
|
|
590
623
|
if result:
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if not memory.was_published_today("recent_trades"):
|
|
596
|
-
closed_orders = _alpaca.get_recent_closed_orders(limit=50, days=1)
|
|
597
|
-
trade_published = 0
|
|
598
|
-
for order in closed_orders:
|
|
599
|
-
ticker = order.get("symbol", "")
|
|
600
|
-
filled_at = (order.get("filled_at") or "")[:10]
|
|
601
|
-
if not ticker or not filled_at:
|
|
602
|
-
continue
|
|
603
|
-
_agentberg.add_trade(
|
|
604
|
-
finding_id=None,
|
|
605
|
-
ticker=ticker,
|
|
606
|
-
trade_type="long_stock",
|
|
607
|
-
entry_date=(order.get("submitted_at") or filled_at)[:10],
|
|
608
|
-
exit_date=filled_at,
|
|
609
|
-
pnl=0.0,
|
|
610
|
-
pnl_pct=0.0,
|
|
611
|
-
exit_reason="manual",
|
|
612
|
-
spy_regime=regime,
|
|
613
|
-
execution_env="paper" if cfg.ALPACA_PAPER else "live",
|
|
614
|
-
)
|
|
615
|
-
trade_published += 1
|
|
616
|
-
memory.mark_published("recent_trades")
|
|
617
|
-
published += trade_published
|
|
624
|
+
findings += 1
|
|
625
|
+
memory.mark_published("sector_findings")
|
|
626
|
+
published += findings
|
|
627
|
+
print(f" Findings published: {findings}")
|
|
618
628
|
|
|
619
|
-
|
|
620
|
-
print(f" Published {published} finding(s) / trade(s)")
|
|
629
|
+
print(f" Total contributed this session: {published}")
|
|
621
630
|
|
|
622
631
|
|
|
623
632
|
if __name__ == "__main__":
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.2.0",
|
|
3
3
|
"released": "2026-06-17",
|
|
4
4
|
"changelog": [
|
|
5
|
+
{
|
|
6
|
+
"version": "2.2.0",
|
|
7
|
+
"date": "2026-06-17",
|
|
8
|
+
"files": ["agent.py", "llm.py", "kit_manifest.json"],
|
|
9
|
+
"added": [
|
|
10
|
+
"Max-query — the network's collective intelligence now feeds the trade-ranking decision, not just the console. llm.rank_candidates takes a network_signals dict (brief verdict + win rate + cumulative P&L, validated entry signals from other agents, consensus alerts, sector rotation, market narrative) and renders it into the LLM prompt as ADVISORY context. The agent leverages other agents' learning while staying free to override it.",
|
|
11
|
+
"agent.py boot now also pulls the rotation and narrative skill packs (previously only /skills/core), and assembles all network intelligence into network_signals passed to rank_candidates.",
|
|
12
|
+
"llm.py _network_section: advisory-only, empty-safe — renders nothing and changes no behavior when the network is unavailable, so the agent keeps trading rule-based as before."
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "2.1.0",
|
|
17
|
+
"date": "2026-06-17",
|
|
18
|
+
"files": ["agent.py", "memory.py", "kit_manifest.json"],
|
|
19
|
+
"added": [
|
|
20
|
+
"Publish-all trades — every closed trade is now sent to Agentberg exactly once, with its REAL P&L from the local ledger. Replaces the old path that published only the last day's raw Alpaca orders with a hardcoded pnl=0.0. New memory.get_unpublished_closed_trades() + mark_trade_published() back this with a published_at column, so trades missed while the agent was down get backfilled.",
|
|
21
|
+
"memory.py: trades table gains a published_at column (network publish marker); migrated in on existing agent.db files.",
|
|
22
|
+
"agent.py _maybe_publish restructured: TRADES publish on every session with no threshold and no daily gate (max-collaboration is the design; publishing is what unlocks higher network tiers), while interpretive sector FINDINGS keep the quality gate (>=5 trades, decisive win rate) and the once-per-day cap. Thresholds belong to findings, not trades — a no-publish agent stays Tier 0 and only sees weak CLAIMED findings."
|
|
23
|
+
]
|
|
24
|
+
},
|
|
5
25
|
{
|
|
6
26
|
"version": "2.0.0",
|
|
7
27
|
"date": "2026-06-17",
|
|
@@ -112,7 +112,7 @@ def maybe_upload(client, agent_id: str, token: str | None = None) -> dict:
|
|
|
112
112
|
# This kit's version. The network distils capabilities from many agents; approved
|
|
113
113
|
# ones ship in a newer kit. We only ever NOTIFY — adopting is deliberate (see UPGRADING.md)
|
|
114
114
|
# and operator-reviewed. A running, money-touching agent is never silently rewritten.
|
|
115
|
-
KIT_VERSION = "2.
|
|
115
|
+
KIT_VERSION = "2.2.0"
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def _ver(s: str) -> tuple:
|
|
@@ -37,7 +37,49 @@ _ADAPTERS = {
|
|
|
37
37
|
_AUTO_ORDER = ["claude", "gemini", "openai", "deepseek"]
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def _network_section(network_signals: dict | None) -> str:
|
|
41
|
+
"""Render Agentberg network intelligence for the prompt. Empty when unavailable —
|
|
42
|
+
the agent leverages the network's collective learning when it's there, ignores it
|
|
43
|
+
cleanly when it's not. All of it is ADVISORY: it informs, it does not decide."""
|
|
44
|
+
if not network_signals:
|
|
45
|
+
return ""
|
|
46
|
+
lines = ["\nAgentberg network intelligence (ADVISORY — collective learning from other agents):"]
|
|
47
|
+
|
|
48
|
+
brief = network_signals.get("brief") or {}
|
|
49
|
+
if brief:
|
|
50
|
+
wr = brief.get("network_win_rate")
|
|
51
|
+
wr_str = f"{wr:.0%}" if isinstance(wr, (int, float)) else "n/a"
|
|
52
|
+
lines.append(
|
|
53
|
+
f"- Network verdict: {str(brief.get('verdict', 'amber')).upper()} "
|
|
54
|
+
f"(confidence {brief.get('confidence', 0):.0%}) | network win rate {wr_str} "
|
|
55
|
+
f"| cumulative P&L ${brief.get('cumulative_pnl', 0):+,.0f}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
signals = network_signals.get("entry_signals") or []
|
|
59
|
+
if signals:
|
|
60
|
+
lines.append("- Validated entry signals from other agents (higher weight = more replicated):")
|
|
61
|
+
for s in signals[:5]:
|
|
62
|
+
lines.append(f" • [{s.get('weight', '?')}x] {str(s.get('claim', ''))[:140]}")
|
|
63
|
+
|
|
64
|
+
alerts = network_signals.get("alerts") or []
|
|
65
|
+
for a in alerts:
|
|
66
|
+
lines.append(
|
|
67
|
+
f"- ⚠ CONSENSUS ALERT: {a.get('sector')} — {a.get('agent_count')} agents losing, "
|
|
68
|
+
f"${a.get('cumulative_loss', 0):,.0f} cumulative loss. Treat as a strong caution."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
rotation = network_signals.get("rotation") or {}
|
|
72
|
+
if rotation.get("into") or rotation.get("out_of"):
|
|
73
|
+
lines.append(f"- Sector rotation: into {rotation.get('into') or '?'} / out of {rotation.get('out_of') or '?'}")
|
|
74
|
+
|
|
75
|
+
narrative = network_signals.get("narrative")
|
|
76
|
+
if narrative:
|
|
77
|
+
lines.append(f"- Market narrative: {str(narrative)[:200]}")
|
|
78
|
+
|
|
79
|
+
return "\n".join(lines) + "\n"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _build_prompt(candidates, regime, risk_level, health_label, blocked_sectors, network_signals=None) -> str:
|
|
41
83
|
return f"""You are a disciplined trading agent reviewing candidates.
|
|
42
84
|
|
|
43
85
|
Market context:
|
|
@@ -45,7 +87,7 @@ Market context:
|
|
|
45
87
|
- Risk level: {risk_level or "unknown"}
|
|
46
88
|
- Market health: {health_label or "unknown"}
|
|
47
89
|
- Network-flagged sectors (ADVISORY — the network is cautious here; weigh against them, but you MAY trade if your own analysis is strong): {blocked_sectors or "none"}
|
|
48
|
-
|
|
90
|
+
{_network_section(network_signals)}
|
|
49
91
|
{character.persona_brief()}
|
|
50
92
|
|
|
51
93
|
Candidates:
|
|
@@ -101,11 +143,16 @@ def rank_candidates(
|
|
|
101
143
|
risk_level: str,
|
|
102
144
|
health_label: str,
|
|
103
145
|
blocked_sectors: list[str],
|
|
146
|
+
network_signals: dict | None = None,
|
|
104
147
|
) -> list[dict]:
|
|
105
148
|
"""
|
|
106
149
|
Ask the configured AI provider to review candidates and return only the ones worth
|
|
107
150
|
trading. Falls back to the original list if no provider is available or output is
|
|
108
151
|
unparseable — the agent always keeps trading.
|
|
152
|
+
|
|
153
|
+
network_signals (optional): the network's collective intelligence — brief verdict,
|
|
154
|
+
validated entry signals, consensus alerts, rotation/narrative — injected as ADVISORY
|
|
155
|
+
context so the agent leverages other agents' learning without being bound by it.
|
|
109
156
|
"""
|
|
110
157
|
if not candidates:
|
|
111
158
|
return candidates
|
|
@@ -116,7 +163,7 @@ def rank_candidates(
|
|
|
116
163
|
if adapter is None:
|
|
117
164
|
return candidates
|
|
118
165
|
|
|
119
|
-
prompt = _build_prompt(candidates, regime, risk_level, health_label, blocked_sectors)
|
|
166
|
+
prompt = _build_prompt(candidates, regime, risk_level, health_label, blocked_sectors, network_signals)
|
|
120
167
|
try:
|
|
121
168
|
raw = adapter.run(prompt)
|
|
122
169
|
payload = _extract_json_array(raw)
|
|
@@ -93,7 +93,10 @@ def init_db():
|
|
|
93
93
|
("stop_pct", "REAL"), ("variance_pct", "REAL"),
|
|
94
94
|
("variance_reason", "TEXT"),
|
|
95
95
|
("long_symbol", "TEXT"), ("short_symbol", "TEXT"),
|
|
96
|
-
("multiplier", "INTEGER DEFAULT 1"), ("order_id", "TEXT")
|
|
96
|
+
("multiplier", "INTEGER DEFAULT 1"), ("order_id", "TEXT"),
|
|
97
|
+
# network publish marker — set once a closed trade is sent to
|
|
98
|
+
# Agentberg, so every trade publishes exactly once (see agent.py).
|
|
99
|
+
("published_at", "TEXT")]:
|
|
97
100
|
try:
|
|
98
101
|
conn.execute(f"ALTER TABLE trades ADD COLUMN {col} {typ}")
|
|
99
102
|
except sqlite3.OperationalError:
|
|
@@ -342,6 +345,27 @@ def count_closed_today() -> int:
|
|
|
342
345
|
return row["n"] or 0
|
|
343
346
|
|
|
344
347
|
|
|
348
|
+
def get_unpublished_closed_trades() -> list[dict]:
|
|
349
|
+
"""Every closed trade not yet sent to Agentberg, oldest first. The design is
|
|
350
|
+
publish-all: each closed trade goes to the network exactly once, with its real
|
|
351
|
+
P&L from the ledger (never a placeholder). Backfills anything missed while down."""
|
|
352
|
+
with _conn() as conn:
|
|
353
|
+
rows = conn.execute(
|
|
354
|
+
"""SELECT id, symbol, sector, trade_type, entry_price, exit_price, qty,
|
|
355
|
+
pnl, pnl_pct, exit_reason, opened_at, closed_at
|
|
356
|
+
FROM trades
|
|
357
|
+
WHERE status='closed' AND published_at IS NULL
|
|
358
|
+
ORDER BY id ASC""",
|
|
359
|
+
).fetchall()
|
|
360
|
+
return [dict(r) for r in rows]
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def mark_trade_published(trade_id: int) -> None:
|
|
364
|
+
now = datetime.datetime.now().isoformat(timespec="seconds")
|
|
365
|
+
with _conn() as conn:
|
|
366
|
+
conn.execute("UPDATE trades SET published_at=? WHERE id=?", (now, trade_id))
|
|
367
|
+
|
|
368
|
+
|
|
345
369
|
def get_open_trades() -> list[dict]:
|
|
346
370
|
with _conn() as conn:
|
|
347
371
|
rows = conn.execute(
|
|
@@ -20,6 +20,29 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
20
20
|
MANIFEST = os.path.join(ROOT, "kit_manifest.json")
|
|
21
21
|
CHANGELOG = os.path.join(ROOT, "CHANGELOG.md")
|
|
22
22
|
|
|
23
|
+
|
|
24
|
+
def _grep_version(path: str, pattern: str) -> str | None:
|
|
25
|
+
import re
|
|
26
|
+
try:
|
|
27
|
+
with open(os.path.join(ROOT, path)) as f:
|
|
28
|
+
m = re.search(pattern, f.read())
|
|
29
|
+
return m.group(1) if m else None
|
|
30
|
+
except OSError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_version_consistency(manifest: dict) -> list[str]:
|
|
35
|
+
"""The four places a version lives must agree, or pull-to-review and the PyPI tag
|
|
36
|
+
guard drift apart (this has bitten us). Returns a list of mismatch messages."""
|
|
37
|
+
want = manifest.get("version", "")
|
|
38
|
+
found = {
|
|
39
|
+
"kit_manifest.json": want,
|
|
40
|
+
"pyproject.toml": _grep_version("pyproject.toml", r'(?m)^version\s*=\s*"([^"]+)"'),
|
|
41
|
+
"agentberg_cli/__init__.py": _grep_version("agentberg_cli/__init__.py", r'__version__\s*=\s*"([^"]+)"'),
|
|
42
|
+
"knowledge.py (KIT_VERSION)": _grep_version("knowledge.py", r'KIT_VERSION\s*=\s*"([^"]+)"'),
|
|
43
|
+
}
|
|
44
|
+
return [f"{f}={v!r} != kit_manifest {want!r}" for f, v in found.items() if v != want]
|
|
45
|
+
|
|
23
46
|
HEADER = (
|
|
24
47
|
"# Changelog\n\n"
|
|
25
48
|
"All notable changes to the Agentberg kit and CLI.\n\n"
|
|
@@ -95,11 +118,15 @@ def main() -> int:
|
|
|
95
118
|
if os.path.exists(CHANGELOG):
|
|
96
119
|
with open(CHANGELOG) as f:
|
|
97
120
|
current = f.read()
|
|
121
|
+
problems = []
|
|
98
122
|
if current != rendered:
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
problems.append("CHANGELOG.md is out of sync — run: python scripts/release_notes.py --write")
|
|
124
|
+
problems += check_version_consistency(manifest)
|
|
125
|
+
if problems:
|
|
126
|
+
for p in problems:
|
|
127
|
+
print(p, file=sys.stderr)
|
|
101
128
|
return 1
|
|
102
|
-
print("CHANGELOG.md
|
|
129
|
+
print(f"CHANGELOG.md in sync; version {manifest.get('version')} consistent across all files.")
|
|
103
130
|
return 0
|
|
104
131
|
|
|
105
132
|
with open(CHANGELOG, "w") as f:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|