collisions
¶
Merge away stale duplicate instruments, using the other brokers to say which of two names is the live one.
python -m stock_brokers.instruments.mapping.utilities.collisions
python -m stock_brokers.instruments.mapping.utilities.collisions --date 2026-09-09
python -m stock_brokers.instruments.mapping.utilities.collisions --date 2026-09-09 --dry-run
python -m stock_brokers.instruments.mapping.utilities.collisions --date 2026-09-09 --delete-ties
A broker's token space is per exchange segment rather than global, so one number meaning different things in different segments is expected and is never touched here. What is worth acting on is a broker publishing two rows for one scrip inside a single namespace of its own: Flattrade lists BSE token 500040 twice, once as CENTURYTEX and once as ABREL, which are the old and the new name of one company after a rename. Both rows carry a real symbol and both hash to a valid identity, so the mapping cannot tell them apart on its own and ends up with a live instrument and a ghost.
Two tests have to agree before anything is merged, because each one alone produces false positives.
The first is the broker's own raw file. A collision only counts when that broker lists the token more than once inside one of its own token namespaces, which is the exchange and segment columns named in TOKEN_NAMESPACES. Without this test the pass also fires on Stoxkart's NSE cash and NSE currency derivatives rows, which share a token number, both land in nse_fixed_income because the canonical vocabulary has one nse exchange, and are two genuinely different government bonds. Those appear once per namespace rather than twice within one, so this test excludes them.
The second is the other brokers. For each colliding instrument, count how many other brokers mapped that same identity on the same date, and keep the one carried by strictly more of them than any rival. Nine brokers call BSE 500040 ABREL and none has heard of CENTURYTEX, so that vote is unanimous, but a stale name is often still carried by one or two brokers whose files have the same rename lag: NSE token 1467 is JSWDULUX at nine brokers and AKZOINDIA at one. A majority settles both shapes, where a rule requiring every rival to have no support at all would settle only the first.
When that count ties, a narrower one breaks it: how many other brokers carry the identity under this same token. BSE token 530565 is why. Flattrade carries both POPEES and KOIYAIN on it, Stoxkart carries KOIYAIN on it, and Stoxkart carries POPEES on a different token, because POPEES is a real company on another scrip code. Counted anywhere the vote is one against one; counted on this token it is one against zero, and the narrower count is the one that says which name belongs to this scrip.
Nothing is merged when both counts are level, since there is then no evidence to prefer either. Those are reported and left alone, because a wrong merge is worse than a duplicate, which the segment filter and the cache's candidate list already handle correctly. Passing --delete-ties merges them anyway, keeping the lowest instrument id; that last choice is arbitrary rather than reasoned, so it is opt in and the daily job never uses it. No mapped date currently needs it.
Merging deletes the unsupported broker mapping rows, never the master rows. The supported instrument is already mapped by this broker under this token, so what remains is one token pointing at one instrument, which is the case the coverage report already describes as a broker listing one instrument on several rows. Master rows stay because they are a dated record of what was seen and because a past date may still map them.
This runs as the last step of the daily mapping, after every broker, because it cannot ask a broker anything until that broker is written. It is safe to repeat, and re-mapping a single broker afterwards brings that broker's duplicates back until it is run again.
Functions:
| Name | Description |
|---|---|
latest_mapping_date |
The most recent date anything was mapped on. |
read_collisions |
Read every token that points at more than one instrument inside a single segment, with each candidate's cross-broker support. |
repeated_tokens |
Of a broker's tokens, the ones its own raw file lists more than once inside one of its token namespaces. |
decide |
Decide which of one collision's candidates survives and which are merged into it. |
describe |
Read a readable name for each instrument, for the report this pass prints. |
delete_mappings |
Delete the merged broker mapping rows, all of them in one transaction. |
duplicates_in_broker_files |
Narrow the collisions to the ones a broker's own raw file actually repeats within one namespace. |
run |
Merge one date's stale duplicate instruments away and report what was done. |
main |
Parse the command line arguments and merge the duplicates. |
latest_mapping_date
¶
The most recent date anything was mapped on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A SQLAlchemy engine over TimescaleDB. |
required |
Returns:
| Type | Description |
|---|---|
date | None
|
The latest mapping date, or None when nothing has been mapped. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
read_collisions
¶
Read every token that points at more than one instrument inside a single segment, with each candidate's cross-broker support.
Each candidate carries two counts. Its support is the number of other brokers that mapped the same instrument identity anywhere on the same date, which is the evidence used to tell a live instrument from a stale alias of one. Its token support is the narrower number that mapped that identity under this same token, which is what separates two real companies whose names have both been attached to one scrip code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A SQLAlchemy engine over TimescaleDB. |
required |
mapping_date
|
date
|
The mapping date to examine. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Keyed by the tuple (broker, segment, broker_token), each value a list of (instrument_id, supporters, token_supporters) tuples ordered by instrument id. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
repeated_tokens
¶
Of a broker's tokens, the ones its own raw file lists more than once inside one of its token namespaces.
A token appearing once in each of two namespaces is that broker numbering its markets separately and is not a duplicate. A token appearing twice inside one namespace is the broker publishing two rows for one scrip, which is the only case this module acts on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A SQLAlchemy engine over TimescaleDB. |
required |
mapping_date
|
date
|
The snapshot date of the raw rows to read. |
required |
broker
|
str
|
The broker whose raw table to read. |
required |
broker_tokens
|
list[str]
|
The tokens to test. |
required |
Returns:
| Type | Description |
|---|---|
set
|
The subset of broker_tokens that repeat inside one namespace. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
decide
¶
Decide which of one collision's candidates survives and which are merged into it.
The candidate carried by strictly more other brokers than any of its rivals wins. Requiring every rival to have no support at all would be too blunt: a stale name is often still carried by one or two other brokers whose files have the same rename lag, so nine brokers against one would be left unsettled beside a genuine one against one.
When that count ties, token support breaks it: the candidate another broker maps under this same token is the one that belongs to this scrip. BSE token 530565 is the case this exists for. Flattrade carries both POPEES and KOIYAIN on it, Stoxkart carries KOIYAIN on it, and Stoxkart also carries POPEES on a different token entirely, because POPEES is a real company on another scrip code. Counting identities anywhere makes that one against one; counting them on this token makes it one against zero, and the second count is the one telling the truth about which name belongs here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
list
|
The (instrument_id, supporters, token_supporters) tuples of one collision. |
required |
break_ties
|
bool
|
When True, settle a collision that both counts leave level by keeping the lowest instrument id rather than declining. That last choice is arbitrary and only deterministic, so this is opt in. |
False
|
Returns:
| Type | Description |
|---|---|
tuple
|
(keep, merge), where keep is the surviving instrument id or None when the evidence does not settle the collision, and merge is the list of instrument ids to delete. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
describe
¶
Read a readable name for each instrument, for the report this pass prints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A SQLAlchemy engine over TimescaleDB. |
required |
instrument_identifiers
|
list[str]
|
The instrument ids to name. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Instrument id to a display name, falling back to the id itself. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
delete_mappings
¶
Delete the merged broker mapping rows, all of them in one transaction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A SQLAlchemy engine over TimescaleDB. |
required |
mapping_date
|
date
|
The mapping date to delete from. |
required |
deletions
|
list
|
Tuples of (broker, broker_token, instrument_id) to remove. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The number of rows deleted. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
duplicates_in_broker_files
¶
Narrow the collisions to the ones a broker's own raw file actually repeats within one namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A SQLAlchemy engine over TimescaleDB. |
required |
mapping_date
|
date
|
The mapping date the collisions belong to. |
required |
collisions
|
dict
|
The collisions read by read_collisions. |
required |
Returns:
| Type | Description |
|---|---|
tuple
|
(duplicates, namespaced), two lists of collision keys, the first being real duplicates in the broker's file and the second being tokens the broker numbers separately per market. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
run
¶
Merge one date's stale duplicate instruments away and report what was done.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mapping_date
|
date | None
|
The date to resolve. Defaults to the latest mapped date. |
None
|
dry_run
|
bool
|
When True, report the decisions without deleting anything. |
False
|
break_ties
|
bool
|
When True, also merge the collisions where the vote is tied, keeping the lowest instrument id. The choice is arbitrary, so this is never on by default and never on in the daily job. |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Keys "collisions", "duplicates", "merged", "unsettled" and "deleted". |
Raises:
| Type | Description |
|---|---|
SystemExit
|
If nothing has been mapped and no date was given. |
Source code in stock_brokers/instruments/mapping/utilities/collisions.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | |
main
¶
Parse the command line arguments and merge the duplicates.
Returns:
| Type | Description |
|---|---|
None
|
This function returns nothing. |