IMHO, asyncio.set_event_loop() and policy.get_event_loop()/policy.set_event_loop() are not deprecated by oversight.
In IPython, I think you could use new_event_loop() for getting a new loop instance.
Then, save the loop reference somewhere as a direct attribute, threading.local or ContextVar.
Calling loop.run_until_complete() looks pretty normal in your situation.
At my job, we have Runner class, the basic usage is:
with Runner() as runner:
runner.run(async_func())
The implementation is pretty close to asyncio.run() but runner.run(...) can be called multiple times. Async context manager interface is responsible for resource closing.
Maybe I should extract the implementation into a third-party library, I've found this concept useful for CLI applications at least.