chore(task): archive 09-06-capital-radar-daily-detail

This commit is contained in:
yuxuanhui
2026-09-25 23:48:21 +08:00
parent 39f8fe5fbf
commit 1197b78f9a
13 changed files with 3 additions and 3 deletions
@@ -0,0 +1,62 @@
"""Collect one real sector into an isolated PostgreSQL snapshot store.
Run with the server's uv environment from zhixing-server. No production
publication is created; facts must be read back from this store for validation.
"""
from datetime import date
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
import json
import time
import tushare as ts
from zhixing_server.bootstrap.config import Settings
from zhixing_server.modules.sector_radar.domain.source import build_source_snapshot
from zhixing_server.modules.sector_radar.infrastructure.postgres import PostgresSectorRadarRepository
def main():
"""Persist provider responses before inspecting their values; redact errors."""
settings = Settings(_env_file='../.env')
url = urlsplit(settings.database_url)
host = url.netloc.rsplit('@', 1)[0] + '@127.0.0.1:5433'
database = urlunsplit((url.scheme, host, '/radar_detail_selftest_0906', url.query, ''))
repository = PostgresSectorRadarRepository(database, max_connections=2)
client = ts.pro_api(settings.tushare_token)
target = date(2026, 9, 4)
counts = []
def collect(api, fields, **params):
"""Store each raw response atomically and return persisted rows."""
time.sleep(0.25)
frame = client.query(api, fields=fields, **params)
snapshot = build_source_snapshot(api_name=api, params=params,
rows=frame.to_dict('records'), target_trade_date=target)
repository.save_source_snapshots((snapshot,))
counts.append({'api':api, 'rows':snapshot.row_count, 'snapshot':snapshot.snapshot_id})
return snapshot.rows
try:
collect('dc_index','ts_code,trade_date,name,idx_type,level,pct_change,leading_code',
trade_date='20260904', ts_code='BK1147.DC')
members = collect('dc_member','trade_date,ts_code,con_code,name',
trade_date='20260904', ts_code='BK1147.DC')
collect('stock_basic','ts_code,symbol,name,market,exchange,list_status,list_date,delist_date',list_status='L')
collect('suspend_d','ts_code,trade_date,suspend_timing,suspend_type',trade_date='20260904')
collect('trade_cal','exchange,cal_date,is_open,pretrade_date',exchange='SSE',start_date='20260720',end_date='20260904')
for member in members:
code = member['con_code']
collect('daily','ts_code,trade_date,close,pre_close,pct_chg,vol,amount',trade_date='20260904',ts_code=code)
collect('moneyflow_dc','trade_date,ts_code,name,net_amount,net_amount_rate,pct_change,close',trade_date='20260904',ts_code=code)
collect('moneyflow','trade_date,ts_code,net_mf_amount',trade_date='20260904',ts_code=code)
result={'status':'collected','sector':'BK1147.DC','trade_date':str(target),'members':len(members),'snapshots':counts}
except Exception as exc:
result={'status':'partial','snapshots':counts,'error_type':type(exc).__name__,
'message':str(exc).replace(settings.tushare_token,'[redacted]')[:300]}
finally:
repository.close()
Path('../.trellis/tasks/09-06-capital-radar-daily-detail/research/collection-result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2))
print(json.dumps({k:v for k,v in result.items() if k!='snapshots'},ensure_ascii=False))
print('Stored snapshots:',len(counts))
if __name__ == '__main__':
main()
@@ -0,0 +1,243 @@
{
"status": "collected",
"sector": "BK1147.DC",
"trade_date": "2026-09-04",
"members": 14,
"snapshots": [
{
"api": "dc_index",
"rows": 1,
"snapshot": "73f94be4b5c94238b611396b7cdf2e314f17cdd23532c3773febf3e3277f1674"
},
{
"api": "dc_member",
"rows": 14,
"snapshot": "2046df68966d3eda9a392b0254b84576842c42c487ea0a2f1044b6cc75de3afa"
},
{
"api": "stock_basic",
"rows": 5556,
"snapshot": "1c87ce65e081e670add1c1167473f798e97e8c9cf41db3e4ac71df0e3de5bb90"
},
{
"api": "suspend_d",
"rows": 8,
"snapshot": "b91d0981345538463407939a7ed863b8fb8de5e447b8ec731b8767b7ae13aa5d"
},
{
"api": "trade_cal",
"rows": 47,
"snapshot": "afff3cba955380cc0af9cdaac1d8861c17a70b089c70ea95aadb1cc568f8a26e"
},
{
"api": "daily",
"rows": 1,
"snapshot": "4f2c4f92685180b3de23a26c9d0602a8acf3fedb7dbc2413ac92bda76d8fa65a"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "7c1907d19c2db8134a807edba87fe165b44a1e96d6b3731e65a26f52715f6f12"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "521254cf598c28608571973eeec1e52c56c856ee547bbd2d9ceacc4e1a29727e"
},
{
"api": "daily",
"rows": 1,
"snapshot": "9bf1ca69c3be480113c933c25681dab1379e0a068bdf9a31452794de8c4bff94"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "8b81006f266edabf8ac1e2aa292e9b278ca9131eea47f86fbef89373197215f6"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "e90138a791bf7e3d458d3865838ac3aee394fe491ebf93cb363425852d1bca92"
},
{
"api": "daily",
"rows": 1,
"snapshot": "81ca417096a073698375daa03f0a660398d53c3943b16c9598a8faa918ff177f"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "40fb4ad0d2bbe8413e6dfcc213e8efe7f3367f84f492f68e430f1a9d08e9cc51"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "47d32d618d5d8a2118253b1e3d7b354628bae22a7b10880f4cfdd3ee370dae22"
},
{
"api": "daily",
"rows": 1,
"snapshot": "2418e65c6800ef5633e6f260cbe617eb3b5bbda4d71e16e56589cd1603827b6b"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "d3559b1413d931dfe882fa64f501b75c76c91125fa0fbe616b385e9a3b7c9e15"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "76a6e0cbe16e92c707af828a9936d87bd810844c357256c18e4d9ef10d86e40d"
},
{
"api": "daily",
"rows": 1,
"snapshot": "a3307443f9e89ff5523043055509dc309c6766067a5d9eb83bdef5d19e6e44c1"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "8d4c401bf8ade3dae6c0eab35069d59e61d4c6383b190e21a420d7735d0ab5a2"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "ee3a90e0e406940f60358edfa3688f8774687bc768c4e9c069ca6d8337a39bcc"
},
{
"api": "daily",
"rows": 1,
"snapshot": "bf21400564f20c771905d46be0dd225eb9d0579393ab5734038ba93db63042f4"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "66611bdd15e87cd5ccec0f48d7e94282cedb210f0393a313519b6896abf0de1f"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "eb98e9d138478b7ec2ae57557a1e61d34f825fd9faf8f30ef530c4d54f5e391e"
},
{
"api": "daily",
"rows": 1,
"snapshot": "550627fe30dccb760e4dbc73dd0ecee85676f781dac1ba059483e7d83b701091"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "5d1e6b1222b37896ad79d759e8fb8a240fb438bf6d50eda687c56181648c3a12"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "7bc22506f40cd9c550a48699c93bee28f00c8717e304cd26761b33d9e66dbf35"
},
{
"api": "daily",
"rows": 1,
"snapshot": "39dda855afcfcf4e32ccd67e4cd28942f8eed13a171b82fa438000bfcff87252"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "b9042adf9d7c2823a7155691c6c349f451b6d84fe7eb2ca83ab749a51eaa6c6e"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "aed26a777f4d4eae9236b677d6ed7180a093aa49076eeee3b796325db3ce2e9f"
},
{
"api": "daily",
"rows": 1,
"snapshot": "26d7d7beb26a8318f9b1104b5bc7446562663bba3e7142d9957562f5dce16c64"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "560d0e8e0a1d1220acab7a4d22ba6aadb4d47555344f75bbdbaf7650d62d95bf"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "7f6759aea68eb193c271c6f8230de9237bcd25bb445eef786adc5b39d5527e08"
},
{
"api": "daily",
"rows": 1,
"snapshot": "467d8fa44adf1aeecb81bb970a3239db4f48c75ec680bd802310ba0a40b9a92f"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "cc80509ebf2c47e85cb1eebe0735db7c84facd01eba73212db218fbabcf6c980"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "7f145408f62626e955b83617821e4c41ce0af3353533de790de22211f02ba36c"
},
{
"api": "daily",
"rows": 1,
"snapshot": "e515db0257399b1433fc14c4062a020a4837dfe6ff968cd70ab2b60f1035794e"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "31aa693eb092cf03a7b1e0da65ffd46e79389831f93809e6738789bfa6e8d619"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "b4073fdbd1e5f73c52d64aae07ea3dd8a9613f12cdea316abcbbaf4bdff6f28e"
},
{
"api": "daily",
"rows": 1,
"snapshot": "ffd0d7d09fee0dc7e5a035c9131220042563a8d5c852ff5ca8d0a4d8b7ba8004"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "7e9bff54c0b00afc12bbcc7a98dfcd7e504ce780e47ac7a3c3443e989d9da031"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "fd045165e72b1127df96bd73487a99d2b561c641783a0dfa3b0ed8b470e4abda"
},
{
"api": "daily",
"rows": 1,
"snapshot": "30ccd5069b7a8c361cc5408ecb9e0013f36d314cf5dde4c6d79b9e10f8942315"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "bbcc7b25b1e2d51487dcb76b28404966db7e83c9153af8f3119e1653f20f8618"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "c210e7f840c5baecc94d6fd333f73fd98b994b65575f2ce38faed428e59b8f9f"
},
{
"api": "daily",
"rows": 1,
"snapshot": "bbb69dfcf4862e214b3a47265ba3607476695cb9147eed3d9f8199a0e7501ff1"
},
{
"api": "moneyflow_dc",
"rows": 1,
"snapshot": "fce61a39b30de56dc3b898f6d8e4bf156504bedc001c5c85abfceaecbb490f36"
},
{
"api": "moneyflow",
"rows": 1,
"snapshot": "2e0739949d25df4a56631c761e064121ff8f99ead4e95306c1c1f1bfa44111ae"
}
]
}
@@ -0,0 +1,20 @@
{
"trade_date": "2026-09-04",
"sector": "BK1147.DC",
"members": 14,
"pct_change": 1.82,
"main_net_yuan": "485456500.00",
"turnover_yuan": "2615472881.48000",
"daily_ratio": "0.1856094564915916866216729052",
"active_net_yuan": "-255455700.00",
"rows_by_api": {
"dc_index": 1,
"dc_member": 14,
"stock_basic": 5556,
"suspend_d": 8,
"trade_cal": 47,
"daily": 14,
"moneyflow_dc": 14,
"moneyflow": 14
}
}
@@ -0,0 +1,77 @@
"""Replay the persisted real SPD sample in an isolated database, with no network.
The one-sector publication is exclusively a local integration fixture; its ranks
must never be interpreted as a market-wide ranking.
"""
import os
import json
from datetime import date
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
import psycopg
from alembic import command
from alembic.config import Config
from zhixing_server.bootstrap.config import Settings, get_settings
from zhixing_server.modules.sector_radar.application.build import BuildSectorRadar, BuildSectorRadarCommand
from zhixing_server.modules.sector_radar.domain.models import SectorType
from zhixing_server.modules.sector_radar.domain.source import (
SourceSnapshot, SourceResult, TradeCalendarRow, SectorIndexRow, SectorMemberRow,
StockBasicRow, SuspendRow, DailyRow, MoneyflowDcRow, MoneyflowRow, build_source_snapshot,
)
from zhixing_server.modules.sector_radar.infrastructure.postgres import PostgresSectorRadarRepository
def database_url():
"""Resolve only the named localhost fixture database without printing secrets."""
s=Settings(_env_file='../.env'); u=urlsplit(s.database_url)
return urlunsplit((u.scheme,u.netloc.rsplit('@',1)[0]+'@127.0.0.1:5433','/radar_detail_selftest_0906',u.query,''))
class StoredSource:
"""Implement provider port using already committed raw snapshots only."""
def __init__(self, dsn):
self.by_api={}
with psycopg.connect(dsn) as c:
for r in c.execute('SELECT id,api_name,normalized_params,target_trade_date,partition_key,observed_at,payload,row_count,returned_fields,content_sha256,row_limit,limit_reached FROM sector_radar_source_snapshot').fetchall():
snap=SourceSnapshot(r[0],r[1],tuple(sorted(r[2].items())),r[3],r[4],r[5],tuple(r[6]),r[7],tuple(r[8]),r[9],r[10],r[11])
self.by_api.setdefault(r[1],[]).append(snap)
def result(self, api, parser):
snaps=tuple(self.by_api[api])
return SourceResult(snaps,tuple(parser(row) for s in snaps for row in s.rows))
def fetch_trade_calendar(self,start,end):
return self.result('trade_cal',TradeCalendarRow.from_mapping)
def fetch_sector_indices(self,target,kind):
if kind is SectorType.INDUSTRY:
# Explicitly empty industry scope for this one-concept test fixture.
snap=build_source_snapshot(api_name='dc_index',params={'test_scope':'empty_industry'},rows=(),target_trade_date=target)
return SourceResult((snap,),())
return self.result('dc_index',lambda row:SectorIndexRow.from_mapping(row,kind))
def fetch_sector_members(self,target,codes):
return self.result('dc_member',SectorMemberRow.from_mapping)
def fetch_stock_basics(self):
return self.result('stock_basic',StockBasicRow.from_mapping)
def fetch_suspensions(self,target):
return self.result('suspend_d',SuspendRow.from_mapping)
def fetch_daily(self,target):
return self.result('daily',DailyRow.from_mapping)
def fetch_moneyflow_dc(self,target,codes):
return self.result('moneyflow_dc',MoneyflowDcRow.from_mapping)
def fetch_moneyflow(self,target):
return self.result('moneyflow',MoneyflowRow.from_mapping)
def main():
"""Run actual build and HTTP reads, checking independently recomputed facts."""
dsn=database_url()
os.environ['ZHIXING_DATABASE_URL']=dsn
get_settings.cache_clear()
command.upgrade(Config('alembic.ini'),'head')
repository=PostgresSectorRadarRepository(dsn,max_connections=2)
result=BuildSectorRadar(StoredSource(dsn),repository,today=date(2026,9,6)).execute(BuildSectorRadarCommand(trade_date=date(2026,9,4)))
print(json.dumps(result.as_dict(),ensure_ascii=False))
repository.close()
if __name__=='__main__':
main()
@@ -0,0 +1,27 @@
"""Verify API facts against independently calculated persisted real data."""
import json
from decimal import Decimal
from pathlib import Path
from urllib.request import urlopen
root='http://127.0.0.1:8016/api/v1/sector-radar/'
d=json.load(urlopen(root+'sectors/concept/BK1147.DC/detail?trade_date=2026-09-04'))
assert d['status']=='success'
assert len(d['members'])==14
assert Decimal(d['pct_change'])==Decimal('1.82')
assert sum(Decimal(m['net_amount_yuan']) for m in d['members'])==Decimal('485456500')
assert sum(Decimal(m['active_buy_net_amount_yuan']) for m in d['members'])==Decimal('-255455700')
assert Decimal(d['summary']['amount']['metric_value'])==Decimal('4.854565')
# Storage uses 12 fractional digits for persisted metric observations.
assert abs(Decimal(d['summary']['ratio']['metric_value'])-Decimal('0.18560945649159168662'))<Decimal('1e-12')
assert d['history']['available_days']==1
assert len(d['history']['points'])==30
assert d['summary']['swing']['missing'] is True
assert len(d['leaders']['pct_change']['top'])==5
for view in ['amount','ratio']:
r=json.load(urlopen(root+'rankings?sector_type=concept&view='+view+'&side=top&trade_date=2026-09-04'))['rows'][0]
assert Decimal(r['pct_change'])==Decimal('1.82')
assert Decimal(r['daily_net_amount_yuan'])==Decimal('485456500')
assert r['on_list_count']==1
Path(__file__).with_name('verified-detail.json').write_text(json.dumps(d,ensure_ascii=False,indent=2))
print('PASS: real persisted SPD data matches detail and both ranking APIs; 14 members, 30 slots / 1 available day, absent swing remains null.')